1# Copyright 2023 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Tests for the pw_build.gn_writer module.""" 15 16import os 17import unittest 18 19from io import StringIO 20from pathlib import PurePath 21from tempfile import TemporaryDirectory 22 23from pw_build.gn_config import GnConfig 24from pw_build.gn_writer import ( 25 COPYRIGHT_HEADER, 26 GnFile, 27 GnWriter, 28) 29from pw_build.gn_target import GnTarget 30from pw_build.gn_utils import MalformedGnError 31 32 33class TestGnWriter(unittest.TestCase): 34 """Tests for gn_writer.GnWriter.""" 35 36 def setUp(self): 37 """Creates a GnWriter that writes to a StringIO.""" 38 self.reset() 39 40 def reset(self): 41 """Resets the writer and output.""" 42 self.output = StringIO() 43 self.writer = GnWriter(self.output) 44 45 def test_write_comment(self): 46 """Writes a GN comment.""" 47 self.writer.write_comment('hello, world!') 48 self.assertEqual( 49 self.output.getvalue(), 50 '# hello, world!\n', 51 ) 52 53 def test_write_comment_wrap(self): 54 """Writes a GN comment that is exactly 80 characters.""" 55 extra_long = ( 56 "This line is a " + ("really, " * 5) + "REALLY extra long comment" 57 ) 58 self.writer.write_comment(extra_long) 59 self.assertEqual( 60 self.output.getvalue(), 61 '# This line is a really, really, really, really, really, REALLY ' 62 'extra long\n# comment\n', 63 ) 64 65 def test_write_comment_nowrap(self): 66 """Writes a long GN comment without whitespace to wrap on.""" 67 no_breaks = 'A' + ('a' * 76) + 'h!' 68 self.writer.write_comment(no_breaks) 69 self.assertEqual( 70 self.output.getvalue(), 71 '# Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 72 'aaaaaaaaaaaaah!\n', 73 ) 74 75 def test_write_imports(self): 76 """Writes GN import statements.""" 77 self.writer.write_import('foo.gni') 78 self.writer.write_imports(['bar.gni', 'baz.gni']) 79 lines = [ 80 'import("foo.gni")', 81 'import("bar.gni")', 82 'import("baz.gni")', 83 ] 84 self.assertEqual('\n'.join(lines), self.output.getvalue().strip()) 85 86 def test_write_config(self): 87 """Writes a GN config.""" 88 config = GnConfig( 89 json='''{ 90 "label": "$dir_3p/test:my-config", 91 "cflags": ["-frobinator", "-fizzbuzzer"], 92 "defines": ["KEY=VAL"], 93 "public": true, 94 "usages": 1 95 }''' 96 ) 97 self.writer.write_config(config) 98 lines = [ 99 'config("my-config") {', 100 ' cflags = [', 101 ' "-fizzbuzzer",', 102 ' "-frobinator",', 103 ' ]', 104 ' defines = [', 105 ' "KEY=VAL",', 106 ' ]', 107 '}', 108 ] 109 self.assertEqual('\n'.join(lines), self.output.getvalue().strip()) 110 111 def test_write_target(self): 112 """Tests writing the target using a GnWriter.""" 113 target = GnTarget( 114 '$build', 115 '$src', 116 json='''{ 117 "target_type": "custom_type", 118 "target_name": "my-target", 119 "package": "my-package" 120 }''', 121 ) 122 target.add_visibility(bazel='//visibility:private') 123 target.add_visibility(bazel='//foo:__subpackages__') 124 target.add_path('public', bazel='//foo:my-header.h') 125 target.add_path('sources', bazel='//foo:my-source.cc') 126 target.add_path('inputs', bazel='//bar:my.data') 127 target.config.add('cflags', '-frobinator') 128 target.add_dep(public=True, bazel='//my-package:foo') 129 target.add_dep(public=True, bazel='@com_corp_repo//bar') 130 target.add_dep(bazel='//other-pkg/baz') 131 target.add_dep(bazel='@com_corp_repo//:top-level') 132 133 output = StringIO() 134 writer = GnWriter(output) 135 writer.repos = {'com_corp_repo': 'repo'} 136 writer.aliases = {'$build/other-pkg/baz': '$build/another-pkg/baz'} 137 writer.write_target(target) 138 139 self.assertEqual( 140 output.getvalue(), 141 ''' 142# Generated from //my-package:my-target 143custom_type("my-target") { 144 visibility = [ 145 "../foo/*", 146 ":*", 147 ] 148 public = [ 149 "$src/foo/my-header.h", 150 ] 151 sources = [ 152 "$src/foo/my-source.cc", 153 ] 154 inputs = [ 155 "$src/bar/my.data", 156 ] 157 cflags = [ 158 "-frobinator", 159 ] 160 public_deps = [ 161 "$dir_pw_third_party/repo/bar", 162 ":foo", 163 ] 164 deps = [ 165 "$dir_pw_third_party/repo:top-level", 166 "../another-pkg/baz", 167 ] 168} 169'''.lstrip(), 170 ) 171 172 def test_write_target_public_visibility(self): 173 """Tests writing a globbaly visible target using a GnWriter.""" 174 target = GnTarget( 175 '$build', 176 '$src', 177 json='''{ 178 "target_type": "custom_type", 179 "target_name": "my-target", 180 "package": "my-package" 181 }''', 182 ) 183 target.add_visibility(bazel='//visibility:private') 184 target.add_visibility(bazel='//visibility:public') 185 186 output = StringIO() 187 writer = GnWriter(output) 188 writer.repos = {'com_corp_repo': 'repo'} 189 writer.aliases = {'$build/other-pkg/baz': '$build/another-pkg/baz'} 190 writer.write_target(target) 191 192 self.assertEqual( 193 output.getvalue(), 194 ''' 195# Generated from //my-package:my-target 196custom_type("my-target") { 197} 198'''.lstrip(), 199 ) 200 201 def test_write_list(self): 202 """Writes a GN list assigned to a variable.""" 203 self.writer.write_list('empty', []) 204 self.writer.write_list('items', ['foo', 'bar', 'baz']) 205 lines = [ 206 'items = [', 207 ' "bar",', 208 ' "baz",', 209 ' "foo",', 210 ']', 211 ] 212 self.assertEqual('\n'.join(lines), self.output.getvalue().strip()) 213 214 def test_write_scope(self): 215 """Writes a GN scope assigned to a variable.""" 216 self.writer.write_scope('outer') 217 self.writer.write('key1 = "val1"') 218 self.writer.write_scope('inner') 219 self.writer.write('key2 = "val2"') 220 self.writer.write_end() 221 self.writer.write('key3 = "val3"') 222 self.writer.write_end() 223 lines = [ 224 'outer = {', 225 ' key1 = "val1"', 226 ' inner = {', 227 ' key2 = "val2"', 228 ' }', 229 '', 230 ' key3 = "val3"', 231 '}', 232 ] 233 self.assertEqual('\n'.join(lines), self.output.getvalue().strip()) 234 235 def test_write_if_else_end(self): 236 """Writes GN conditional statements.""" 237 self.writer.write_if('current_os == "linux"') 238 self.writer.write('mascot = "penguin"') 239 self.writer.write_else_if('current_os == "mac"') 240 self.writer.write('mascot = "dogcow"') 241 self.writer.write_else_if('current_os == "win"') 242 self.writer.write('mascot = "clippy"') 243 self.writer.write_else() 244 self.writer.write('mascot = "dropbear"') 245 self.writer.write_end() 246 lines = [ 247 'if (current_os == "linux") {', 248 ' mascot = "penguin"', 249 '} else if (current_os == "mac") {', 250 ' mascot = "dogcow"', 251 '} else if (current_os == "win") {', 252 ' mascot = "clippy"', 253 '} else {', 254 ' mascot = "dropbear"', 255 '}', 256 ] 257 self.assertEqual('\n'.join(lines), self.output.getvalue().strip()) 258 259 def test_write_unclosed_target(self): 260 """Triggers an error from an unclosed GN scope.""" 261 self.writer.write_target_start('unclosed', 'target') 262 with self.assertRaises(MalformedGnError): 263 self.writer.seal() 264 265 def test_write_unclosed_scope(self): 266 """Triggers an error from an unclosed GN scope.""" 267 self.writer.write_scope('unclosed_scope') 268 with self.assertRaises(MalformedGnError): 269 self.writer.seal() 270 271 def test_write_unclosed_if(self): 272 """Triggers an error from an unclosed GN condition.""" 273 self.writer.write_if('var == "unclosed-if"') 274 with self.assertRaises(MalformedGnError): 275 self.writer.seal() 276 277 def test_write_unclosed_else_if(self): 278 """Triggers an error from an unclosed GN condition.""" 279 self.writer.write_if('var == "closed-if"') 280 self.writer.write_else_if('var == "unclosed-else-if"') 281 with self.assertRaises(MalformedGnError): 282 self.writer.seal() 283 284 def test_write_unclosed_else(self): 285 """Triggers an error from an unclosed GN condition.""" 286 self.writer.write_if('var == "closed-if"') 287 self.writer.write_else_if('var == "closed-else-if"') 288 self.writer.write_else() 289 with self.assertRaises(MalformedGnError): 290 self.writer.seal() 291 292 293class TestGnFile(unittest.TestCase): 294 """Tests for gn_writer.GnFile.""" 295 296 def test_format_on_close(self): 297 """Verifies the GN file is formatted when the file is closed.""" 298 with TemporaryDirectory() as tmpdirname: 299 with GnFile(PurePath(tmpdirname, 'BUILD.gn')) as build_gn: 300 build_gn.write(' correct = "indent"') 301 build_gn.write_comment('newline before comment') 302 build_gn.write_scope('no_newline_before_item') 303 build_gn.write_list('single_item', ['is.inlined']) 304 build_gn.write_end() 305 306 filename = PurePath('pw_build', 'gn_writer.py') 307 expected = ( 308 COPYRIGHT_HEADER 309 + f''' 310# This file was automatically generated by {filename} 311 312correct = "indent" 313 314# newline before comment 315no_newline_before_item = {{ 316 single_item = [ "is.inlined" ] 317}}''' 318 ) 319 with open(os.path.join(tmpdirname, 'BUILD.gn'), 'r') as build_gn: 320 self.assertEqual(expected.strip(), build_gn.read().strip()) 321 322 323if __name__ == '__main__': 324 unittest.main() 325