blob: 8e88a6a8a323d9264546df2c6e056dd62ad3edad [file] [log] [blame] [edit]
# Copyright 2024 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import tempfile
import unittest
from pathlib import Path
_SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(_SCRIPT_DIR))
sys.path.insert(1, str(_SCRIPT_DIR / "../bazel/scripts"))
import ninja_artifacts
from ninja_artifacts import MockNinjaRunner
class NinjaArtifactsTest(unittest.TestCase):
def test_get_last_build_targets(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
build_dir = Path(temp_dir)
# If the file doesn't exist, default to [":default"].
self.assertListEqual(
ninja_artifacts.get_last_build_targets(build_dir), [":default"]
)
# If the file is empty, default to [":default"] too.
(build_dir / ninja_artifacts.LAST_NINJA_TARGETS_FILE).write_text("")
self.assertListEqual(
ninja_artifacts.get_last_build_targets(build_dir), [":default"]
)
(build_dir / ninja_artifacts.LAST_NINJA_TARGETS_FILE).write_text(
" foo"
)
self.assertListEqual(
ninja_artifacts.get_last_build_targets(build_dir), ["foo"]
)
(build_dir / ninja_artifacts.LAST_NINJA_TARGETS_FILE).write_text(
"foo bar"
)
self.assertListEqual(
ninja_artifacts.get_last_build_targets(build_dir),
["foo", "bar"],
)
def test_get_build_plan_deps(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
build_dir = Path(temp_dir)
(build_dir / ninja_artifacts.NINJA_BUILD_PLAN_DEPS_FILE).write_text(
"build.ninja.stamp: dep1 dep2 dep3 dep4\n"
)
self.assertListEqual(
ninja_artifacts.get_build_plan_deps(build_dir),
["dep1", "dep2", "dep3", "dep4"],
)
def test_check_output_needs_update(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
build_dir = Path(temp_dir)
input_files = [build_dir / "input1", build_dir / "input2"]
output_file = build_dir / "output"
# output file does not exist, nor any input file.
self.assertTrue(
ninja_artifacts.check_output_needs_update(
output_file, input_files
)
)
# output file does not exist, but input files do.
input_files[0].write_text("one")
input_files[1].write_text("two")
self.assertTrue(
ninja_artifacts.check_output_needs_update(
output_file, input_files
)
)
# output file does exist, and is newer than inputs.
output_file.write_text("out")
self.assertFalse(
ninja_artifacts.check_output_needs_update(
output_file, input_files
)
)
# output file does exist, but is older than one input.
output_stat = output_file.stat()
os.utime(
input_files[1],
times=(output_stat.st_atime, output_stat.st_mtime + 1),
)
self.assertTrue(
ninja_artifacts.check_output_needs_update(
output_file, input_files
)
)
def test_get_last_build_artifacts(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
# Setup fake source and build directory.
build_gn_path = Path(temp_dir) / "BUILD.gn"
build_gn_path.write_text("# Fake BUILD.gn\n")
build_dir = Path(temp_dir) / "out"
build_dir.mkdir(parents=True)
build_ninja_d_path = (
build_dir / ninja_artifacts.NINJA_BUILD_PLAN_DEPS_FILE
)
build_ninja_d_path.write_text("build.ninja.stamp: ../BUILD.gn")
last_targets_path = (
build_dir / ninja_artifacts.LAST_NINJA_TARGETS_FILE
)
last_targets_path.write_text("foo")
# Create mock NinjaRunner instance to avoid calling Ninja binary.
ninja_runner = MockNinjaRunner(
build_dir, "bar\nfoo\nzoo\n'quoted'\n"
)
self.assertListEqual(
ninja_artifacts.get_last_build_artifacts(ninja_runner),
["bar", "foo", "zoo", "'quoted'"],
)
self.assertListEqual(
ninja_runner.last_ninja_args(), ["-t", "outputs", "foo"]
)
last_ninja_artifacts_path = (
build_dir / ninja_artifacts.LAST_NINJA_ARTIFACTS_FILE
)
self.assertTrue(last_ninja_artifacts_path.exists())
self.assertEqual(
last_ninja_artifacts_path.read_text(), "bar\nfoo\nzoo\n'quoted'"
)
# Modify last_ninja_build_targets.txt and verify the cache was regenerated.
last_targets_path.write_text("bar zoo")
last_targets_stat = last_targets_path.stat()
os.utime(
last_targets_path,
times=(
last_targets_stat.st_atime,
last_targets_stat.st_mtime + 1,
),
)
ninja_runner = MockNinjaRunner(build_dir, "second\ncall\n")
self.assertListEqual(
ninja_artifacts.get_last_build_artifacts(ninja_runner),
["second", "call"],
)
self.assertListEqual(
ninja_runner.last_ninja_args(), ["-t", "outputs", "bar", "zoo"]
)
self.assertEqual(
last_ninja_artifacts_path.read_text(), "second\ncall"
)
def test_get_last_build_sources(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
# Setup fake source and build directory.
build_gn_path = Path(temp_dir) / "BUILD.gn"
build_gn_path.write_text("# Fake BUILD.gn\n")
build_dir = Path(temp_dir) / "out"
build_dir.mkdir(parents=True)
build_ninja_d_path = (
build_dir / ninja_artifacts.NINJA_BUILD_PLAN_DEPS_FILE
)
build_ninja_d_path.write_text("build.ninja.stamp: ../BUILD.gn")
last_targets_path = (
build_dir / ninja_artifacts.LAST_NINJA_TARGETS_FILE
)
last_targets_path.write_text("foo")
# Create mock NinjaRunner instance to avoid calling Ninja binary.
ninja_runner = MockNinjaRunner(
build_dir,
"../src/foo\n../src/bar\noutput_file\nout_dir/out_file\n../src/zoo\n",
)
self.assertListEqual(
ninja_artifacts.get_last_build_sources(ninja_runner),
["../src/foo", "../src/bar", "../src/zoo"],
)
self.assertListEqual(
ninja_runner.last_ninja_args(),
[
"-t",
"inputs",
"--no-shell-escape",
"--dependency-order",
"foo",
],
)
last_ninja_sources_path = (
build_dir / ninja_artifacts.LAST_NINJA_SOURCES_FILE
)
self.assertTrue(last_ninja_sources_path.exists())
self.assertEqual(
last_ninja_sources_path.read_text(),
"../src/foo\n../src/bar\n../src/zoo",
)
# Modify last_ninja_build_targets.txt and verify the cache was regenerated.
last_targets_path.write_text("bar zoo")
last_targets_stat = last_targets_path.stat()
os.utime(
last_targets_path,
times=(
last_targets_stat.st_atime,
last_targets_stat.st_mtime + 1,
),
)
ninja_runner = MockNinjaRunner(build_dir, "../second\n../call\n")
self.assertListEqual(
ninja_artifacts.get_last_build_sources(ninja_runner),
["../second", "../call"],
)
self.assertListEqual(
ninja_runner.last_ninja_args(),
[
"-t",
"inputs",
"--no-shell-escape",
"--dependency-order",
"bar",
"zoo",
],
)
self.assertEqual(
last_ninja_sources_path.read_text(), "../second\n../call"
)
class ShouldChangedFilesTriggerBuildTest(unittest.TestCase):
def setUp(self) -> None:
self._td = tempfile.TemporaryDirectory()
self.root = Path(self._td.name)
self.build_dir = self.root / "out/build"
self.build_dir.mkdir(parents=True)
(
self.build_dir / ninja_artifacts.NINJA_BUILD_PLAN_DEPS_FILE
).write_text(
"build.ninja.stamp: ../../BUILD.gn ../../src/foo.gni dep1 dep2 dep3 dep4"
)
def tearDown(self) -> None:
self._td.cleanup()
def test_no_change(self) -> None:
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["some/file.txt"], self.root, MockNinjaRunner(self.build_dir, "")
)
self.assertFalse(result)
self.assertEqual(reason, "")
def test_build_file_changes(self) -> None:
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["BUILD.gn"],
self.root,
MockNinjaRunner(self.build_dir, ""),
)
self.assertEqual(reason, "GN build graph changed.")
self.assertTrue(result)
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["src/foo.gni"],
self.root,
MockNinjaRunner(self.build_dir, ""),
)
self.assertEqual(reason, "GN build graph changed.")
self.assertTrue(result)
MockNinjaRunner(self.build_dir, "")
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["other/BUILD.gn", "src/bar.gni"],
self.root,
MockNinjaRunner(self.build_dir, ""),
)
self.assertEqual(reason, "")
self.assertFalse(result)
def test_source_file_changes(self) -> None:
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["some/file.txt"],
self.root,
MockNinjaRunner(self.build_dir, ":default\t../../some/file.txt"),
)
self.assertEqual(reason, "Sources updated for target: :default")
self.assertTrue(result)
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["some/file.txt"],
self.root,
MockNinjaRunner(
self.build_dir, ":default\t../../some/other_file.txt"
),
)
self.assertEqual(reason, "")
self.assertFalse(result)
# Simulate a previous build of 'foo' instead of ':default' and verify that
# only when related source have change does
(self.build_dir / ninja_artifacts.LAST_NINJA_TARGETS_FILE).write_text(
"foo bar"
)
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["some/file.txt"],
self.root,
MockNinjaRunner(
self.build_dir,
"foo\t../../src/foo.cc\nfoo\t../../src/foo.h\nbar\t../../src/bar.cc\n",
),
)
self.assertEqual(reason, "")
self.assertFalse(result)
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["src/foo.h", "src/qux.cc"],
self.root,
MockNinjaRunner(
self.build_dir,
"foo\t../../src/foo.cc\nfoo\t../../src/foo.h\nbar\t../../src/bar.cc\n",
),
)
self.assertEqual(reason, "Sources updated for target: foo")
self.assertTrue(result)
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["src/foo.cc", "src/bar.cc"],
self.root,
MockNinjaRunner(
self.build_dir,
"foo\t../../src/foo.cc\nfoo\t../../src/foo.h\nbar\t../../src/bar.cc\n",
),
)
self.assertEqual(reason, "Sources updated for 2 targets.")
self.assertTrue(result)
def test_root_targets(self) -> None:
# If root_targets is not specific, verify that the fallback ":default" is used
# when invoking the Ninja tool.
mock_runner = MockNinjaRunner(
self.build_dir, ":default\t../../some/file.txt"
)
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["some/file.txt"], self.root, mock_runner
)
self.assertListEqual(
mock_runner.last_ninja_args(),
["-t", "multi-inputs", "--depfile", ":default"],
)
mock_runner = MockNinjaRunner(
self.build_dir,
":first\t../../some/file.txt\n:second\t../../src.foo.cc",
)
result, reason = ninja_artifacts.should_file_changes_trigger_build(
["some/file.txt"],
self.root,
mock_runner,
root_targets=[":first", ":second"],
)
self.assertListEqual(
mock_runner.last_ninja_args(),
["-t", "multi-inputs", "--depfile", ":first", ":second"],
)
if __name__ == "__main__":
unittest.main()