#!/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 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 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) 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._outdir / 'fake_source_set.file_a.cc.o', self._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._outdir / 'fake_test.fake_test.cc.o', self._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._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) class ExpandExpressionsTest(unittest.TestCase): """Tests expansion of expressions like .""" 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) 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, '')), ['']) def test_incomplete_expression(self): for incomplete_expression in [ '', '', '--arg=', ]: 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 [ ('', path), ('--arg=', f'--arg={path}'), ('--argument=;' '', 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, '') def test_target_file_non_existent_target(self): with self.assertRaisesRegex(ExpressionError, 'generated'): expand_expressions(self._paths, '') def test_target_file_if_exists(self): path = self._path('test', 'fake_test.elf', create=True) for expr, expected in [ ('', path), ('--arg=', f'--arg={path}'), ('--argument=;' '', 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 [ '', '', '', '--arg=', '--argument=;' '', ]: self.assertEqual(list(expand_expressions(self._paths, expr)), []) def test_target_file_if_exists_error_if_never_has_artifact(self): for expr in [ '' 'bar=' '', '--foo=', ]: with self.assertRaises(ExpressionError): expand_expressions(self._paths, expr) def test_target_objects(self): self.assertEqual( set( expand_expressions( self._paths, '')), { 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, '')), { 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, '')), []) def test_target_objects_other_content_in_arg(self): for arg in [ '--foo=', 'bar', '--foobar', '' '', '' '', ]: 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, '') 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) if __name__ == '__main__': unittest.main()