1#!/usr/bin/env python3 2# Copyright 2020 The Pigweed Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not 5# use this file except in compliance with the License. You may obtain a copy of 6# the License at 7# 8# https://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15"""Tests for the Python runner.""" 16 17import os 18from pathlib import Path 19import platform 20import tempfile 21import unittest 22 23from pw_build.gn_resolver import ExpressionError, GnPaths, Label, TargetInfo 24from pw_build.gn_resolver import expand_expressions 25 26ROOT = Path(r'C:\gn_root' if platform.system() == 'Windows' else '/gn_root') 27 28TEST_PATHS = GnPaths( 29 ROOT, 30 ROOT / 'out', 31 ROOT / 'some' / 'cwd', 32 '//toolchains/cool:ToolChain', 33) 34 35 36class LabelTest(unittest.TestCase): 37 """Tests GN label parsing.""" 38 39 def setUp(self): 40 self._paths_and_toolchain_name = [ 41 (TEST_PATHS, 'ToolChain'), 42 (GnPaths(*TEST_PATHS[:3], ''), ''), 43 ] 44 45 def test_root(self): 46 for paths, toolchain in self._paths_and_toolchain_name: 47 label = Label(paths, '//') 48 self.assertEqual(label.name, '') 49 self.assertEqual(label.dir, ROOT) 50 self.assertEqual( 51 label.out_dir, ROOT.joinpath('out', toolchain, 'obj') 52 ) 53 self.assertEqual( 54 label.gen_dir, ROOT.joinpath('out', toolchain, 'gen') 55 ) 56 57 def test_absolute(self): 58 for paths, toolchain in self._paths_and_toolchain_name: 59 label = Label(paths, '//foo/bar:baz') 60 self.assertEqual(label.name, 'baz') 61 self.assertEqual(label.dir, ROOT.joinpath('foo/bar')) 62 self.assertEqual( 63 label.out_dir, ROOT.joinpath('out', toolchain, 'obj/foo/bar') 64 ) 65 self.assertEqual( 66 label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/foo/bar') 67 ) 68 69 def test_absolute_implicit_target(self): 70 for paths, toolchain in self._paths_and_toolchain_name: 71 label = Label(paths, '//foo/bar') 72 self.assertEqual(label.name, 'bar') 73 self.assertEqual(label.dir, ROOT.joinpath('foo/bar')) 74 self.assertEqual( 75 label.out_dir, ROOT.joinpath('out', toolchain, 'obj/foo/bar') 76 ) 77 self.assertEqual( 78 label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/foo/bar') 79 ) 80 81 def test_relative(self): 82 for paths, toolchain in self._paths_and_toolchain_name: 83 label = Label(paths, ':tgt') 84 self.assertEqual(label.name, 'tgt') 85 self.assertEqual(label.dir, ROOT.joinpath('some/cwd')) 86 self.assertEqual( 87 label.out_dir, ROOT.joinpath('out', toolchain, 'obj/some/cwd') 88 ) 89 self.assertEqual( 90 label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/some/cwd') 91 ) 92 93 def test_relative_subdir(self): 94 for paths, toolchain in self._paths_and_toolchain_name: 95 label = Label(paths, 'tgt') 96 self.assertEqual(label.name, 'tgt') 97 self.assertEqual(label.dir, ROOT.joinpath('some/cwd/tgt')) 98 self.assertEqual( 99 label.out_dir, 100 ROOT.joinpath('out', toolchain, 'obj/some/cwd/tgt'), 101 ) 102 self.assertEqual( 103 label.gen_dir, 104 ROOT.joinpath('out', toolchain, 'gen/some/cwd/tgt'), 105 ) 106 107 def test_relative_parent_dir(self): 108 for paths, toolchain in self._paths_and_toolchain_name: 109 label = Label(paths, '..:tgt') 110 self.assertEqual(label.name, 'tgt') 111 self.assertEqual(label.dir, ROOT.joinpath('some')) 112 self.assertEqual( 113 label.out_dir, ROOT.joinpath('out', toolchain, 'obj/some') 114 ) 115 self.assertEqual( 116 label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/some') 117 ) 118 119 120class ResolvePathTest(unittest.TestCase): 121 """Tests GN path resolution.""" 122 123 def test_resolve_absolute(self): 124 self.assertEqual(TEST_PATHS.resolve('//'), TEST_PATHS.root) 125 self.assertEqual( 126 TEST_PATHS.resolve('//foo/bar'), TEST_PATHS.root / 'foo' / 'bar' 127 ) 128 self.assertEqual( 129 TEST_PATHS.resolve('//foo/../baz'), TEST_PATHS.root / 'baz' 130 ) 131 132 def test_resolve_relative(self): 133 self.assertEqual(TEST_PATHS.resolve(''), TEST_PATHS.cwd) 134 self.assertEqual(TEST_PATHS.resolve('foo'), TEST_PATHS.cwd / 'foo') 135 self.assertEqual(TEST_PATHS.resolve('..'), TEST_PATHS.root / 'some') 136 137 138NINJA_EXECUTABLE = '''\ 139defines = 140framework_dirs = 141include_dirs = -I../fake_module/public 142cflags = -g3 -Og -fdiagnostics-color -g -fno-common -Wall -Wextra -Werror 143cflags_c = 144cflags_cc = -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register 145target_output_name = this_is_a_test 146 147build fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o: fake_toolchain_cxx ../fake_module/fake_test.cc 148 source_file_dir = ../fake_module 149 source_file_name = fake_test.cc 150 151build fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o: fake_toolchain_cc ../fake_module/fake_test_c.c 152 153build fake_toolchain/obj/fake_module/test/fake_test.elf fake_toolchain/obj/fake_module/test/fake_test.map: fake_toolchain_link fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o 154 ldflags = -Og -fdiagnostics-color 155 libs = 156 frameworks = 157 output_extension = 158 output_dir = host_clang_debug/obj/fake_module/test 159''' 160 161_SOURCE_SET_TEMPLATE = '''\ 162defines = 163framework_dirs = 164include_dirs = -I../fake_module/public 165cflags = -g3 -Og -fdiagnostics-color -g -fno-common -Wall -Wextra -Werror 166cflags_c = 167cflags_cc = -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register 168target_output_name = this_is_a_test 169 170build fake_toolchain/obj/fake_module/fake_source_set.file_a.cc.o: fake_toolchain_cxx ../fake_module/file_a.cc 171 source_file_dir = ../fake_module 172 source_file_name = file_a.cc 173 174build fake_toolchain/obj/fake_module/fake_source_set.file_b.c.o: fake_toolchain_cc ../fake_module/file_b.c 175 176build {path} fake_toolchain/obj/fake_module/fake_source_set.file_a.cc.o fake_toolchain/obj/fake_module/fake_source_set.file_b.c.o 177 ldflags = -Og -fdiagnostics-color -Wno-error=deprecated 178 libs = 179 frameworks = 180 output_extension = 181 output_dir = host_clang_debug/obj/fake_module 182''' 183 184# GN originally used empty .stamp files to mark the completion of a group of 185# dependencies. GN switched to using 'phony' Ninja targets instead, which don't 186# require creating a new file. 187_PHONY_BUILD_PATH = 'fake_toolchain/phony/fake_module/fake_source_set: phony' 188_STAMP_BUILD_PATH = 'fake_toolchain/obj/fake_module/fake_source_set.stamp:' 189 190NINJA_SOURCE_SET = _SOURCE_SET_TEMPLATE.format(path=_PHONY_BUILD_PATH) 191NINJA_SOURCE_SET_STAMP = _SOURCE_SET_TEMPLATE.format(path=_STAMP_BUILD_PATH) 192 193 194def _create_ninja_files(source_set: str) -> tuple: 195 tempdir = tempfile.TemporaryDirectory(prefix='pw_build_test_') 196 197 module = Path(tempdir.name, 'out', 'fake_toolchain', 'obj', 'fake_module') 198 os.makedirs(module) 199 module.joinpath('fake_test.ninja').write_text(NINJA_EXECUTABLE) 200 module.joinpath('fake_source_set.ninja').write_text(source_set) 201 module.joinpath('fake_no_objects.ninja').write_text('\n') 202 203 outdir = Path(tempdir.name, 'out', 'fake_toolchain', 'obj', 'fake_module') 204 205 paths = GnPaths( 206 root=Path(tempdir.name), 207 build=Path(tempdir.name, 'out'), 208 cwd=Path(tempdir.name, 'some', 'module'), 209 toolchain='//tools:fake_toolchain', 210 ) 211 212 return tempdir, outdir, paths 213 214 215class TargetTest(unittest.TestCase): 216 """Tests querying GN target information.""" 217 218 def setUp(self): 219 self._tempdir, self._outdir, self._paths = _create_ninja_files( 220 NINJA_SOURCE_SET 221 ) 222 223 self._rel_outdir = self._outdir.relative_to(self._paths.build) 224 225 def tearDown(self): 226 self._tempdir.cleanup() 227 228 def test_source_set_artifact(self): 229 target = TargetInfo(self._paths, '//fake_module:fake_source_set') 230 self.assertTrue(target.generated) 231 self.assertIsNone(target.artifact) 232 233 def test_source_set_object_files(self): 234 target = TargetInfo(self._paths, '//fake_module:fake_source_set') 235 self.assertTrue(target.generated) 236 self.assertEqual( 237 set(target.object_files), 238 { 239 self._rel_outdir / 'fake_source_set.file_a.cc.o', 240 self._rel_outdir / 'fake_source_set.file_b.c.o', 241 }, 242 ) 243 244 def test_executable_object_files(self): 245 target = TargetInfo(self._paths, '//fake_module:fake_test') 246 self.assertEqual( 247 set(target.object_files), 248 { 249 self._rel_outdir / 'fake_test.fake_test.cc.o', 250 self._rel_outdir / 'fake_test.fake_test_c.c.o', 251 }, 252 ) 253 254 def test_executable_artifact(self): 255 target = TargetInfo(self._paths, '//fake_module:fake_test') 256 self.assertEqual( 257 target.artifact, self._rel_outdir / 'test' / 'fake_test.elf' 258 ) 259 260 def test_non_existent_target(self): 261 target = TargetInfo( 262 self._paths, '//fake_module:definitely_not_a_real_target' 263 ) 264 self.assertFalse(target.generated) 265 self.assertIsNone(target.artifact) 266 267 def test_non_existent_toolchain(self): 268 target = TargetInfo( 269 self._paths, '//fake_module:fake_source_set(//not_a:toolchain)' 270 ) 271 self.assertFalse(target.generated) 272 self.assertIsNone(target.artifact) 273 274 275class StampTargetTest(TargetTest): 276 """Test with old-style .stamp files instead of phony Ninja targets.""" 277 278 def setUp(self): 279 self._tempdir, self._outdir, self._paths = _create_ninja_files( 280 NINJA_SOURCE_SET_STAMP 281 ) 282 283 self._rel_outdir = self._outdir.relative_to(self._paths.build) 284 285 286class ExpandExpressionsTest(unittest.TestCase): 287 """Tests expansion of expressions like <TARGET_FILE(//foo)>.""" 288 289 def setUp(self): 290 self._tempdir, self._outdir, self._paths = _create_ninja_files( 291 NINJA_SOURCE_SET 292 ) 293 294 def tearDown(self): 295 self._tempdir.cleanup() 296 297 def _path(self, *segments: str, create: bool = False) -> str: 298 path = Path(self._outdir, *segments) 299 if create: 300 os.makedirs(path.parent) 301 path.touch() 302 else: 303 assert not path.exists() 304 return str(path.relative_to(self._paths.build)) 305 306 def test_empty(self): 307 self.assertEqual(list(expand_expressions(self._paths, '')), ['']) 308 309 def test_no_expressions(self): 310 self.assertEqual( 311 list(expand_expressions(self._paths, 'foobar')), ['foobar'] 312 ) 313 self.assertEqual( 314 list(expand_expressions(self._paths, '<NOT_AN_EXPRESSION()>')), 315 ['<NOT_AN_EXPRESSION()>'], 316 ) 317 318 def test_incomplete_expression(self): 319 for incomplete_expression in [ 320 '<TARGET_FILE(', 321 '<TARGET_FILE(//foo)', 322 '<TARGET_FILE(//foo>', 323 '<TARGET_FILE(//foo) >', 324 '--arg=<TARGET_FILE_IF_EXISTS(//foo) Hello>', 325 ]: 326 with self.assertRaises(ExpressionError): 327 expand_expressions(self._paths, incomplete_expression) 328 329 def test_target_file(self): 330 path = self._path('test', 'fake_test.elf') 331 332 for expr, expected in [ 333 ('<TARGET_FILE(//fake_module:fake_test)>', path), 334 ('--arg=<TARGET_FILE(//fake_module:fake_test)>', f'--arg={path}'), 335 ( 336 '--argument=<TARGET_FILE(//fake_module:fake_test)>;' 337 '<TARGET_FILE(//fake_module:fake_test)>', 338 f'--argument={path};{path}', 339 ), 340 ]: 341 self.assertEqual( 342 list(expand_expressions(self._paths, expr)), [expected] 343 ) 344 345 def test_target_objects_no_target_file(self): 346 with self.assertRaisesRegex(ExpressionError, 'no output file'): 347 expand_expressions( 348 self._paths, '<TARGET_FILE(//fake_module:fake_source_set)>' 349 ) 350 351 def test_target_file_non_existent_target(self): 352 with self.assertRaisesRegex(ExpressionError, 'generated'): 353 expand_expressions(self._paths, '<TARGET_FILE(//not_real:abc123)>') 354 355 def test_target_file_if_exists(self): 356 path = self._path('test', 'fake_test.elf', create=True) 357 358 for expr, expected in [ 359 ('<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', path), 360 ( 361 '--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', 362 f'--arg={path}', 363 ), 364 ( 365 '--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;' 366 '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', 367 f'--argument={path};{path}', 368 ), 369 ]: 370 self.assertEqual( 371 list(expand_expressions(self._paths, expr)), [expected] 372 ) 373 374 def test_target_file_if_exists_arg_omitted(self): 375 for expr in [ 376 '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', 377 '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test(fake)>', 378 '<TARGET_FILE_IF_EXISTS(//not_a_module:nothing)>', 379 '--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', 380 '--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;' 381 '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', 382 ]: 383 self.assertEqual(list(expand_expressions(self._paths, expr)), []) 384 385 def test_target_file_if_exists_error_if_never_has_artifact(self): 386 for expr in [ 387 '<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>' 388 'bar=<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>' 389 '<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>', 390 '--foo=<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>', 391 ]: 392 with self.assertRaises(ExpressionError): 393 expand_expressions(self._paths, expr) 394 395 def test_target_objects(self): 396 self.assertEqual( 397 set( 398 expand_expressions( 399 self._paths, 400 '<TARGET_OBJECTS(//fake_module:fake_source_set)>', 401 ) 402 ), 403 { 404 self._path('fake_source_set.file_a.cc.o'), 405 self._path('fake_source_set.file_b.c.o'), 406 }, 407 ) 408 self.assertEqual( 409 set( 410 expand_expressions( 411 self._paths, '<TARGET_OBJECTS(//fake_module:fake_test)>' 412 ) 413 ), 414 { 415 self._path('fake_test.fake_test.cc.o'), 416 self._path('fake_test.fake_test_c.c.o'), 417 }, 418 ) 419 420 def test_target_objects_no_objects(self): 421 self.assertEqual( 422 list( 423 expand_expressions( 424 self._paths, 425 '<TARGET_OBJECTS(//fake_module:fake_no_objects)>', 426 ) 427 ), 428 [], 429 ) 430 431 def test_target_objects_other_content_in_arg(self): 432 for arg in [ 433 '--foo=<TARGET_OBJECTS(//fake_module:fake_no_objects)>', 434 '<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar', 435 '--foo<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar', 436 '<TARGET_OBJECTS(//fake_module:fake_no_objects)>' 437 '<TARGET_OBJECTS(//fake_module:fake_no_objects)>', 438 '<TARGET_OBJECTS(//fake_module:fake_source_set)>' 439 '<TARGET_OBJECTS(//fake_module:fake_source_set)>', 440 ]: 441 with self.assertRaises(ExpressionError): 442 expand_expressions(self._paths, arg) 443 444 def test_target_objects_non_existent_target(self): 445 with self.assertRaisesRegex(ExpressionError, 'generated'): 446 expand_expressions(self._paths, '<TARGET_OBJECTS(//not_real)>') 447 448 449class StampExpandExpressionsTest(TargetTest): 450 """Test with old-style .stamp files instead of phony Ninja targets.""" 451 452 def setUp(self): 453 self._tempdir, self._outdir, self._paths = _create_ninja_files( 454 NINJA_SOURCE_SET_STAMP 455 ) 456 457 self._rel_outdir = self._outdir.relative_to(self._paths.build) 458 459 460if __name__ == '__main__': 461 unittest.main() 462