| #!/usr/bin/env python3 |
| # Copyright 2020 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Tests for the Python runner.""" |
| |
| import os |
| from pathlib import Path |
| import platform |
| import tempfile |
| import unittest |
| |
| from pw_build.python_runner import ExpressionError, GnPaths, Label, TargetInfo |
| from pw_build.python_runner import expand_expressions |
| |
| ROOT = Path(r'C:\gn_root' if platform.system() == 'Windows' else '/gn_root') |
| |
| TEST_PATHS = GnPaths( |
| ROOT, |
| ROOT / 'out', |
| ROOT / 'some' / 'cwd', |
| '//toolchains/cool:ToolChain', |
| ) |
| |
| |
| class LabelTest(unittest.TestCase): |
| """Tests GN label parsing.""" |
| def setUp(self): |
| self._paths_and_toolchain_name = [ |
| (TEST_PATHS, 'ToolChain'), |
| (GnPaths(*TEST_PATHS[:3], ''), ''), |
| ] |
| |
| def test_root(self): |
| for paths, toolchain in self._paths_and_toolchain_name: |
| label = Label(paths, '//') |
| self.assertEqual(label.name, '') |
| self.assertEqual(label.dir, ROOT) |
| self.assertEqual(label.out_dir, |
| ROOT.joinpath('out', toolchain, 'obj')) |
| self.assertEqual(label.gen_dir, |
| ROOT.joinpath('out', toolchain, 'gen')) |
| |
| def test_absolute(self): |
| for paths, toolchain in self._paths_and_toolchain_name: |
| label = Label(paths, '//foo/bar:baz') |
| self.assertEqual(label.name, 'baz') |
| self.assertEqual(label.dir, ROOT.joinpath('foo/bar')) |
| self.assertEqual(label.out_dir, |
| ROOT.joinpath('out', toolchain, 'obj/foo/bar')) |
| self.assertEqual(label.gen_dir, |
| ROOT.joinpath('out', toolchain, 'gen/foo/bar')) |
| |
| def test_absolute_implicit_target(self): |
| for paths, toolchain in self._paths_and_toolchain_name: |
| label = Label(paths, '//foo/bar') |
| self.assertEqual(label.name, 'bar') |
| self.assertEqual(label.dir, ROOT.joinpath('foo/bar')) |
| self.assertEqual(label.out_dir, |
| ROOT.joinpath('out', toolchain, 'obj/foo/bar')) |
| self.assertEqual(label.gen_dir, |
| ROOT.joinpath('out', toolchain, 'gen/foo/bar')) |
| |
| def test_relative(self): |
| for paths, toolchain in self._paths_and_toolchain_name: |
| label = Label(paths, ':tgt') |
| self.assertEqual(label.name, 'tgt') |
| self.assertEqual(label.dir, ROOT.joinpath('some/cwd')) |
| self.assertEqual(label.out_dir, |
| ROOT.joinpath('out', toolchain, 'obj/some/cwd')) |
| self.assertEqual(label.gen_dir, |
| ROOT.joinpath('out', toolchain, 'gen/some/cwd')) |
| |
| def test_relative_subdir(self): |
| for paths, toolchain in self._paths_and_toolchain_name: |
| label = Label(paths, 'tgt') |
| self.assertEqual(label.name, 'tgt') |
| self.assertEqual(label.dir, ROOT.joinpath('some/cwd/tgt')) |
| self.assertEqual( |
| label.out_dir, |
| ROOT.joinpath('out', toolchain, 'obj/some/cwd/tgt')) |
| self.assertEqual( |
| label.gen_dir, |
| ROOT.joinpath('out', toolchain, 'gen/some/cwd/tgt')) |
| |
| def test_relative_parent_dir(self): |
| for paths, toolchain in self._paths_and_toolchain_name: |
| label = Label(paths, '..:tgt') |
| self.assertEqual(label.name, 'tgt') |
| self.assertEqual(label.dir, ROOT.joinpath('some')) |
| self.assertEqual(label.out_dir, |
| ROOT.joinpath('out', toolchain, 'obj/some')) |
| self.assertEqual(label.gen_dir, |
| ROOT.joinpath('out', toolchain, 'gen/some')) |
| |
| |
| class ResolvePathTest(unittest.TestCase): |
| """Tests GN path resolution.""" |
| def test_resolve_absolute(self): |
| self.assertEqual(TEST_PATHS.resolve('//'), TEST_PATHS.root) |
| self.assertEqual(TEST_PATHS.resolve('//foo/bar'), |
| TEST_PATHS.root / 'foo' / 'bar') |
| self.assertEqual(TEST_PATHS.resolve('//foo/../baz'), |
| TEST_PATHS.root / 'baz') |
| |
| def test_resolve_relative(self): |
| self.assertEqual(TEST_PATHS.resolve(''), TEST_PATHS.cwd) |
| self.assertEqual(TEST_PATHS.resolve('foo'), TEST_PATHS.cwd / 'foo') |
| self.assertEqual(TEST_PATHS.resolve('..'), TEST_PATHS.root / 'some') |
| |
| |
| NINJA_EXECUTABLE = '''\ |
| defines = |
| framework_dirs = |
| include_dirs = -I../fake_module/public |
| cflags = -g3 -Og -fdiagnostics-color -g -fno-common -Wall -Wextra -Werror |
| cflags_c = |
| cflags_cc = -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register |
| target_output_name = this_is_a_test |
| |
| build fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o: fake_toolchain_cxx ../fake_module/fake_test.cc |
| source_file_dir = ../fake_module |
| source_file_name = fake_test.cc |
| |
| build fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o: fake_toolchain_cc ../fake_module/fake_test_c.c |
| |
| build 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 |
| ldflags = -Og -fdiagnostics-color |
| libs = |
| frameworks = |
| output_extension = |
| output_dir = host_clang_debug/obj/fake_module/test |
| ''' |
| |
| _SOURCE_SET_TEMPLATE = '''\ |
| defines = |
| framework_dirs = |
| include_dirs = -I../fake_module/public |
| cflags = -g3 -Og -fdiagnostics-color -g -fno-common -Wall -Wextra -Werror |
| cflags_c = |
| cflags_cc = -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register |
| target_output_name = this_is_a_test |
| |
| build fake_toolchain/obj/fake_module/fake_source_set.file_a.cc.o: fake_toolchain_cxx ../fake_module/file_a.cc |
| source_file_dir = ../fake_module |
| source_file_name = file_a.cc |
| |
| build fake_toolchain/obj/fake_module/fake_source_set.file_b.c.o: fake_toolchain_cc ../fake_module/file_b.c |
| |
| build {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 |
| ldflags = -Og -fdiagnostics-color -Wno-error=deprecated |
| libs = |
| frameworks = |
| output_extension = |
| output_dir = host_clang_debug/obj/fake_module |
| ''' |
| |
| # GN originally used empty .stamp files to mark the completion of a group of |
| # dependencies. GN switched to using 'phony' Ninja targets instead, which don't |
| # require creating a new file. |
| _PHONY_BUILD_PATH = 'fake_toolchain/phony/fake_module/fake_source_set: phony' |
| _STAMP_BUILD_PATH = 'fake_toolchain/obj/fake_module/fake_source_set.stamp:' |
| |
| NINJA_SOURCE_SET = _SOURCE_SET_TEMPLATE.format(path=_PHONY_BUILD_PATH) |
| NINJA_SOURCE_SET_STAMP = _SOURCE_SET_TEMPLATE.format(path=_STAMP_BUILD_PATH) |
| |
| |
| def _create_ninja_files(source_set: str) -> tuple: |
| tempdir = tempfile.TemporaryDirectory(prefix='pw_build_test_') |
| |
| module = Path(tempdir.name, 'out', 'fake_toolchain', 'obj', 'fake_module') |
| os.makedirs(module) |
| module.joinpath('fake_test.ninja').write_text(NINJA_EXECUTABLE) |
| module.joinpath('fake_source_set.ninja').write_text(source_set) |
| module.joinpath('fake_no_objects.ninja').write_text('\n') |
| |
| outdir = Path(tempdir.name, 'out', 'fake_toolchain', 'obj', 'fake_module') |
| |
| paths = GnPaths(root=Path(tempdir.name), |
| build=Path(tempdir.name, 'out'), |
| cwd=Path(tempdir.name, 'some', 'module'), |
| toolchain='//tools:fake_toolchain') |
| |
| return tempdir, outdir, paths |
| |
| |
| class TargetTest(unittest.TestCase): |
| """Tests querying GN target information.""" |
| def setUp(self): |
| self._tempdir, self._outdir, self._paths = _create_ninja_files( |
| NINJA_SOURCE_SET) |
| |
| self._rel_outdir = self._outdir.relative_to(self._paths.build) |
| |
| def tearDown(self): |
| self._tempdir.cleanup() |
| |
| def test_source_set_artifact(self): |
| target = TargetInfo(self._paths, '//fake_module:fake_source_set') |
| self.assertTrue(target.generated) |
| self.assertIsNone(target.artifact) |
| |
| def test_source_set_object_files(self): |
| target = TargetInfo(self._paths, '//fake_module:fake_source_set') |
| self.assertTrue(target.generated) |
| self.assertEqual( |
| set(target.object_files), { |
| self._rel_outdir / 'fake_source_set.file_a.cc.o', |
| self._rel_outdir / 'fake_source_set.file_b.c.o', |
| }) |
| |
| def test_executable_object_files(self): |
| target = TargetInfo(self._paths, '//fake_module:fake_test') |
| self.assertEqual( |
| set(target.object_files), { |
| self._rel_outdir / 'fake_test.fake_test.cc.o', |
| self._rel_outdir / 'fake_test.fake_test_c.c.o', |
| }) |
| |
| def test_executable_artifact(self): |
| target = TargetInfo(self._paths, '//fake_module:fake_test') |
| self.assertEqual(target.artifact, |
| self._rel_outdir / 'test' / 'fake_test.elf') |
| |
| def test_non_existent_target(self): |
| target = TargetInfo(self._paths, |
| '//fake_module:definitely_not_a_real_target') |
| self.assertFalse(target.generated) |
| self.assertIsNone(target.artifact) |
| |
| def test_non_existent_toolchain(self): |
| target = TargetInfo( |
| self._paths, '//fake_module:fake_source_set(//not_a:toolchain)') |
| self.assertFalse(target.generated) |
| self.assertIsNone(target.artifact) |
| |
| |
| class StampTargetTest(TargetTest): |
| """Test with old-style .stamp files instead of phony Ninja targets.""" |
| def setUp(self): |
| self._tempdir, self._outdir, self._paths = _create_ninja_files( |
| NINJA_SOURCE_SET_STAMP) |
| |
| self._rel_outdir = self._outdir.relative_to(self._paths.build) |
| |
| |
| class ExpandExpressionsTest(unittest.TestCase): |
| """Tests expansion of expressions like <TARGET_FILE(//foo)>.""" |
| def setUp(self): |
| self._tempdir, self._outdir, self._paths = _create_ninja_files( |
| NINJA_SOURCE_SET) |
| |
| def tearDown(self): |
| self._tempdir.cleanup() |
| |
| def _path(self, *segments: str, create: bool = False) -> str: |
| path = Path(self._outdir, *segments) |
| if create: |
| os.makedirs(path.parent) |
| path.touch() |
| else: |
| assert not path.exists() |
| return str(path.relative_to(self._paths.build)) |
| |
| def test_empty(self): |
| self.assertEqual(list(expand_expressions(self._paths, '')), ['']) |
| |
| def test_no_expressions(self): |
| self.assertEqual(list(expand_expressions(self._paths, 'foobar')), |
| ['foobar']) |
| self.assertEqual( |
| list(expand_expressions(self._paths, '<NOT_AN_EXPRESSION()>')), |
| ['<NOT_AN_EXPRESSION()>']) |
| |
| def test_incomplete_expression(self): |
| for incomplete_expression in [ |
| '<TARGET_FILE(', |
| '<TARGET_FILE(//foo)', |
| '<TARGET_FILE(//foo>', |
| '<TARGET_FILE(//foo) >', |
| '--arg=<TARGET_FILE_IF_EXISTS(//foo) Hello>', |
| ]: |
| with self.assertRaises(ExpressionError): |
| expand_expressions(self._paths, incomplete_expression) |
| |
| def test_target_file(self): |
| path = self._path('test', 'fake_test.elf') |
| |
| for expr, expected in [ |
| ('<TARGET_FILE(//fake_module:fake_test)>', path), |
| ('--arg=<TARGET_FILE(//fake_module:fake_test)>', f'--arg={path}'), |
| ('--argument=<TARGET_FILE(//fake_module:fake_test)>;' |
| '<TARGET_FILE(//fake_module:fake_test)>', |
| f'--argument={path};{path}'), |
| ]: |
| self.assertEqual(list(expand_expressions(self._paths, expr)), |
| [expected]) |
| |
| def test_target_objects_no_target_file(self): |
| with self.assertRaisesRegex(ExpressionError, 'no output file'): |
| expand_expressions(self._paths, |
| '<TARGET_FILE(//fake_module:fake_source_set)>') |
| |
| def test_target_file_non_existent_target(self): |
| with self.assertRaisesRegex(ExpressionError, 'generated'): |
| expand_expressions(self._paths, '<TARGET_FILE(//not_real:abc123)>') |
| |
| def test_target_file_if_exists(self): |
| path = self._path('test', 'fake_test.elf', create=True) |
| |
| for expr, expected in [ |
| ('<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', path), |
| ('--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', |
| f'--arg={path}'), |
| ('--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;' |
| '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', |
| f'--argument={path};{path}'), |
| ]: |
| self.assertEqual(list(expand_expressions(self._paths, expr)), |
| [expected]) |
| |
| def test_target_file_if_exists_arg_omitted(self): |
| for expr in [ |
| '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', |
| '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test(fake)>', |
| '<TARGET_FILE_IF_EXISTS(//not_a_module:nothing)>', |
| '--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', |
| '--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;' |
| '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', |
| ]: |
| self.assertEqual(list(expand_expressions(self._paths, expr)), []) |
| |
| def test_target_file_if_exists_error_if_never_has_artifact(self): |
| for expr in [ |
| '<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>' |
| 'bar=<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>' |
| '<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>', |
| '--foo=<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>', |
| ]: |
| with self.assertRaises(ExpressionError): |
| expand_expressions(self._paths, expr) |
| |
| def test_target_objects(self): |
| self.assertEqual( |
| set( |
| expand_expressions( |
| self._paths, |
| '<TARGET_OBJECTS(//fake_module:fake_source_set)>')), { |
| self._path('fake_source_set.file_a.cc.o'), |
| self._path('fake_source_set.file_b.c.o') |
| }) |
| self.assertEqual( |
| set( |
| expand_expressions( |
| self._paths, '<TARGET_OBJECTS(//fake_module:fake_test)>')), |
| { |
| self._path('fake_test.fake_test.cc.o'), |
| self._path('fake_test.fake_test_c.c.o') |
| }) |
| |
| def test_target_objects_no_objects(self): |
| self.assertEqual( |
| list( |
| expand_expressions( |
| self._paths, |
| '<TARGET_OBJECTS(//fake_module:fake_no_objects)>')), []) |
| |
| def test_target_objects_other_content_in_arg(self): |
| for arg in [ |
| '--foo=<TARGET_OBJECTS(//fake_module:fake_no_objects)>', |
| '<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar', |
| '--foo<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar', |
| '<TARGET_OBJECTS(//fake_module:fake_no_objects)>' |
| '<TARGET_OBJECTS(//fake_module:fake_no_objects)>', |
| '<TARGET_OBJECTS(//fake_module:fake_source_set)>' |
| '<TARGET_OBJECTS(//fake_module:fake_source_set)>', |
| ]: |
| with self.assertRaises(ExpressionError): |
| expand_expressions(self._paths, arg) |
| |
| def test_target_objects_non_existent_target(self): |
| with self.assertRaisesRegex(ExpressionError, 'generated'): |
| expand_expressions(self._paths, '<TARGET_OBJECTS(//not_real)>') |
| |
| |
| class StampExpandExpressionsTest(TargetTest): |
| """Test with old-style .stamp files instead of phony Ninja targets.""" |
| def setUp(self): |
| self._tempdir, self._outdir, self._paths = _create_ninja_files( |
| NINJA_SOURCE_SET_STAMP) |
| |
| self._rel_outdir = self._outdir.relative_to(self._paths.build) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |