blob: 4e965cf531d6d3813bf759e44b7ecfa55e0cd2d6 [file] [log] [blame]
#!/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()