| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2023 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 io |
| import contextlib |
| import unittest |
| from pathlib import Path |
| from unittest import mock |
| |
| from api.proxy import log_pb2 |
| from typing import Dict |
| |
| import action_diff |
| import reproxy_logs |
| import remotetool |
| |
| |
| def add_output_file_digests_to_record( |
| log_record: log_pb2.LogRecord, digests: Dict[Path, str]): |
| log_record.command.output.output_files.extend(digests.keys()) |
| for k, v in digests.items(): |
| log_record.remote_metadata.output_file_digests[str(k)] = v |
| |
| |
| class ActionDifferTests(unittest.TestCase): |
| |
| def test_construction_only(self): |
| left_log_dump = log_pb2.LogDump() |
| right_log_dump = log_pb2.LogDump() |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| def test_trace_output_not_in_record(self): |
| left_log_dump = log_pb2.LogDump() |
| right_log_dump = log_pb2.LogDump() |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| path = Path('does/not/exist.txt') |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| root_causes = list(diff.trace_artifact(path)) |
| |
| check_message = 'File not found' |
| self.assertIn(check_message, out.getvalue()) |
| self.assertIn(str(path), out.getvalue()) |
| self.assertEqual(len(root_causes), 1) |
| self.assertIn(str(path), root_causes[0].explanation[0]) |
| self.assertTrue( |
| any(check_message in line for line in root_causes[0].explanation)) |
| |
| def test_trace_output_already_matches(self): |
| path = Path('obj/foo.o') |
| left_record = log_pb2.LogRecord() |
| right_record = log_pb2.LogRecord() |
| digests = {str(path): 'abcdef/234'} |
| add_output_file_digests_to_record(left_record, digests) |
| add_output_file_digests_to_record(right_record, digests) |
| left_log_dump = log_pb2.LogDump(records=[left_record]) |
| right_log_dump = log_pb2.LogDump(records=[right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| root_causes = list(diff.trace_artifact(path)) |
| |
| check_message = f'Digest of {path} already matches' |
| self.assertIn(check_message, out.getvalue()) |
| self.assertEqual(len(root_causes), 0) |
| |
| def test_trace_output_to_originating_action_with_command_diff(self): |
| path = Path('obj/foo.o') |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = "18723601ce7d/145" |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = "73733ba65ade/145" |
| left_digest = {str(path): 'abcdef/234'} |
| right_digest = {str(path): '987654/234'} |
| add_output_file_digests_to_record(left_record, left_digest) |
| add_output_file_digests_to_record(right_record, right_digest) |
| left_log_dump = log_pb2.LogDump(records=[left_record]) |
| right_log_dump = log_pb2.LogDump(records=[right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| left_action = remotetool.ShowActionResult( |
| command=['twiddle', 'dum'], |
| inputs=dict(), |
| platform=dict(), |
| output_files=left_digest) |
| right_action = remotetool.ShowActionResult( |
| command=['twiddle', 'dee'], # different |
| inputs=dict(), |
| platform=dict(), |
| output_files=right_digest) |
| |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| with mock.patch.object(remotetool.RemoteTool, 'show_action', |
| side_effect=[left_action, right_action |
| ]) as mock_show_action: |
| root_causes = list(diff.trace_artifact(path)) |
| |
| check_message = f'have different remote commands' |
| self.assertIn(check_message, out.getvalue()) |
| self.assertEqual(len(root_causes), 1) |
| self.assertTrue( |
| any(check_message in line for line in root_causes[0].explanation)) |
| |
| def test_trace_output_to_action_with_platform_diff(self): |
| path = Path('obj/foo.o') |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = "18723601ce7d/145" |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = "73733ba65ade/145" |
| left_digest = {str(path): 'abcdef/234'} |
| right_digest = {str(path): '987654/234'} |
| add_output_file_digests_to_record(left_record, left_digest) |
| add_output_file_digests_to_record(right_record, right_digest) |
| left_log_dump = log_pb2.LogDump(records=[left_record]) |
| right_log_dump = log_pb2.LogDump(records=[right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| left_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs=dict(), |
| platform={'key': 'value-A'}, |
| output_files=left_digest) |
| right_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs=dict(), |
| platform={'key': 'value-B'}, # different |
| output_files=right_digest) |
| |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| with mock.patch.object(remotetool.RemoteTool, 'show_action', |
| side_effect=[left_action, right_action |
| ]) as mock_show_action: |
| root_causes = list(diff.trace_artifact(path)) |
| |
| check_message = f'differences in remote action platform' |
| self.assertIn(check_message, out.getvalue()) |
| self.assertEqual(len(root_causes), 1) |
| self.assertTrue( |
| any(check_message in line for line in root_causes[0].explanation)) |
| |
| def test_trace_output_to_action_with_extra_input(self): |
| path = Path('obj/foo.o') |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = "18723601ce7d/145" |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = "73733ba65ade/145" |
| left_digest = {str(path): 'abcdef/234'} |
| right_digest = {str(path): '987654/234'} |
| add_output_file_digests_to_record(left_record, left_digest) |
| add_output_file_digests_to_record(right_record, right_digest) |
| left_log_dump = log_pb2.LogDump(records=[left_record]) |
| right_log_dump = log_pb2.LogDump(records=[right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| left_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs=dict(), |
| platform=dict(), |
| output_files=left_digest) |
| right_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={Path("include/header.h"): "bb987aaad663fd/282"}, # extra |
| platform=dict(), |
| output_files=right_digest) |
| |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| with mock.patch.object(remotetool.RemoteTool, 'show_action', |
| side_effect=[left_action, right_action |
| ]) as mock_show_action: |
| root_causes = list(diff.trace_artifact(path)) |
| |
| check_message = f'Input sets are not identical' |
| self.assertIn(check_message, out.getvalue()) |
| self.assertEqual(len(root_causes), 1) |
| self.assertTrue( |
| any(check_message in line for line in root_causes[0].explanation)) |
| |
| def test_trace_output_to_action_with_different_source_input(self): |
| path = Path('obj/foo.o') |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = "18723601ce7d/145" |
| left_record.command.remote_working_directory = "build/here" |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = "73733ba65ade/145" |
| right_record.command.remote_working_directory = "build/here" |
| left_digest = {str(path): 'abcdef/234'} |
| right_digest = {str(path): '987654/234'} |
| add_output_file_digests_to_record(left_record, left_digest) |
| add_output_file_digests_to_record(right_record, right_digest) |
| left_log_dump = log_pb2.LogDump(records=[left_record]) |
| right_log_dump = log_pb2.LogDump(records=[right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| left_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={Path("include/header.h"): "55773311bb987a/282"}, |
| platform=dict(), |
| output_files={Path(k): v for k, v in left_digest.items()}) |
| right_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={Path("include/header.h"): "bb987aaad663fd/282" |
| }, # different |
| platform=dict(), |
| output_files={Path(k): v for k, v in right_digest.items()}) |
| |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| with mock.patch.object(remotetool.RemoteTool, 'show_action', |
| side_effect=[left_action, right_action |
| ]) as mock_show_action: |
| root_causes = list(diff.trace_artifact(path)) |
| |
| check_message = f'does not come from a remote action' |
| self.assertIn(check_message, out.getvalue()) |
| self.assertEqual(len(root_causes), 1) |
| self.assertTrue( |
| any(check_message in line for line in root_causes[0].explanation)) |
| |
| def test_trace_output_to_action_with_intermediate_input_not_in_record(self): |
| path = Path('obj/foo.o') |
| intermediate = Path("gen/include/header.h") |
| remote_working_dir = Path("build/here") |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = "18723601ce7d/145" |
| left_record.command.remote_working_directory = str(remote_working_dir) |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = "73733ba65ade/145" |
| right_record.command.remote_working_directory = str(remote_working_dir) |
| left_digest = {str(path): 'abcdef/234'} # is an intermediate output |
| right_digest = {str(path): '987654/234'} |
| add_output_file_digests_to_record(left_record, left_digest) |
| add_output_file_digests_to_record(right_record, right_digest) |
| |
| left_log_dump = log_pb2.LogDump(records=[left_record]) |
| right_log_dump = log_pb2.LogDump(records=[right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| # actions for the outer call to trace_artifact() |
| outer_left_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={remote_working_dir / intermediate: "55773311bb987a/282"}, |
| platform=dict(), |
| output_files={Path(k): v for k, v in left_digest.items()}) |
| outer_right_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={remote_working_dir / intermediate: "bb987aaad663fd/282" |
| }, # different |
| platform=dict(), |
| output_files={Path(k): v for k, v in right_digest.items()}) |
| |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| with mock.patch.object(remotetool.RemoteTool, 'show_action', |
| side_effect=[ |
| outer_left_action, |
| outer_right_action, |
| ]) as mock_show_action: |
| root_causes = list(diff.trace_artifact(path)) |
| # outer level of trace_artifact() |
| check_messages = [ |
| # outer level of trace_artifact() |
| f'Remote output file {intermediate} differs', |
| # inner level of trace_artifact() -- the root cause |
| f'File not found among left log\'s action outputs: {intermediate}', |
| ] |
| |
| for m in check_messages: |
| self.assertIn(m, out.getvalue()) |
| |
| self.assertEqual(len(root_causes), 1) |
| self.assertIn(str(intermediate), root_causes[0].explanation[0]) |
| self.assertTrue( |
| any( |
| check_messages[1] in line |
| for line in root_causes[0].explanation)) |
| |
| def test_trace_output_to_action_with_different_intermediate_input(self): |
| path = Path('obj/foo.o') |
| intermediate = Path("gen/include/header.h") |
| source = Path("lib/api.h") |
| remote_working_dir = Path("build/here") |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = "18723601ce7d/145" |
| left_record.command.remote_working_directory = str(remote_working_dir) |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = "73733ba65ade/145" |
| right_record.command.remote_working_directory = str(remote_working_dir) |
| left_digest = {str(path): 'abcdef/234'} # is an intermediate output |
| right_digest = {str(path): '987654/234'} |
| add_output_file_digests_to_record(left_record, left_digest) |
| add_output_file_digests_to_record(right_record, right_digest) |
| |
| inner_left_record = log_pb2.LogRecord() |
| inner_left_record.remote_metadata.action_digest = "22233342341/45" |
| inner_left_record.command.remote_working_directory = str( |
| remote_working_dir) |
| inner_right_record = log_pb2.LogRecord() |
| inner_right_record.remote_metadata.action_digest = "777889112ef/45" |
| inner_right_record.command.remote_working_directory = str( |
| remote_working_dir) |
| inner_left_digest = { |
| str(intermediate): '00abcdef/34' |
| } # is an intermediate output |
| inner_right_digest = {str(intermediate): '00987654/34'} |
| add_output_file_digests_to_record(inner_left_record, inner_left_digest) |
| add_output_file_digests_to_record( |
| inner_right_record, inner_right_digest) |
| |
| left_log_dump = log_pb2.LogDump( |
| records=[left_record, inner_left_record]) |
| right_log_dump = log_pb2.LogDump( |
| records=[inner_right_record, right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| # actions for the outer call to trace_artifact() |
| outer_left_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={remote_working_dir / intermediate: "55773311bb987a/282"}, |
| platform=dict(), |
| output_files={Path(k): v for k, v in left_digest.items()}) |
| outer_right_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={remote_working_dir / intermediate: "bb987aaad663fd/282" |
| }, # different |
| platform=dict(), |
| output_files={Path(k): v for k, v in right_digest.items()}) |
| |
| # actions for the inner call to trace_artifact() |
| inner_left_action = remotetool.ShowActionResult( |
| command=['fiddle'], |
| inputs={source: "aa81557311b987a/189"}, |
| platform=dict(), |
| output_files={Path(k): v for k, v in inner_left_digest.items()}) |
| inner_right_action = remotetool.ShowActionResult( |
| command=['fiddle'], |
| inputs={source: "87be001aa4efbfd/182"}, # different |
| platform=dict(), |
| output_files={Path(k): v for k, v in inner_right_digest.items()}) |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| with mock.patch.object(remotetool.RemoteTool, 'show_action', |
| side_effect=[ |
| outer_left_action, |
| outer_right_action, |
| inner_left_action, |
| inner_right_action, |
| ]) as mock_show_action: |
| root_causes = list(diff.trace_artifact(path)) |
| |
| check_messages = [ |
| # outer level of trace_artifact() |
| f'Remote output file {intermediate} differs', |
| # inner level of trace_artifact() -- root cause |
| f'Input {source} does not come from a remote action', |
| ] |
| |
| for m in check_messages: |
| self.assertIn(m, out.getvalue()) |
| |
| self.assertEqual(len(root_causes), 1) |
| self.assertTrue( |
| any( |
| check_messages[1] in line |
| for line in root_causes[0].explanation)) |
| |
| def test_trace_actions_missing_digest_from_record(self): |
| left_action_digest = "44182360ce7d/76" |
| right_action_digest = "217733ba6ade/76" |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = left_action_digest |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = right_action_digest |
| left_log_dump = log_pb2.LogDump() # omit record |
| right_log_dump = log_pb2.LogDump(records=[right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| left_action = remotetool.ShowActionResult( |
| command=['twiddle', 'dum'], |
| inputs=dict(), |
| platform=dict(), |
| output_files=dict()) |
| right_action = remotetool.ShowActionResult( |
| command=['twiddle', 'dee'], # different |
| inputs=dict(), |
| platform=dict(), |
| output_files=dict()) |
| |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| root_causes = list( |
| diff.trace_actions(left_action_digest, right_action_digest)) |
| |
| check_message = f'Unable to find log record' |
| self.assertIn(check_message, out.getvalue()) |
| self.assertEqual(len(root_causes), 0) |
| |
| def test_trace_actions_with_command_diff(self): |
| left_action_digest = "44182360ce7d/76" |
| right_action_digest = "217733ba6ade/76" |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = left_action_digest |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = right_action_digest |
| left_log_dump = log_pb2.LogDump(records=[left_record]) |
| right_log_dump = log_pb2.LogDump(records=[right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| left_action = remotetool.ShowActionResult( |
| command=['twiddle', 'dum'], |
| inputs=dict(), |
| platform=dict(), |
| output_files=dict()) |
| right_action = remotetool.ShowActionResult( |
| command=['twiddle', 'dee'], # different |
| inputs=dict(), |
| platform=dict(), |
| output_files=dict()) |
| |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| with mock.patch.object(remotetool.RemoteTool, 'show_action', |
| side_effect=[left_action, right_action |
| ]) as mock_show_action: |
| root_causes = list( |
| diff.trace_actions(left_action_digest, right_action_digest)) |
| |
| check_message = f'have different remote commands' |
| self.assertIn(check_message, out.getvalue()) |
| self.assertEqual(len(root_causes), 1) |
| self.assertTrue( |
| any(check_message in line for line in root_causes[0].explanation)) |
| |
| def test_trace_actions_with_different_intermediate_input(self): |
| left_action_digest = "18723601ce7d/145" |
| right_action_digest = "73733ba65ade/145" |
| intermediate = Path("gen/include/header.h") |
| source = Path("lib/api.h") |
| remote_working_dir = Path("build/here") |
| left_record = log_pb2.LogRecord() |
| left_record.remote_metadata.action_digest = left_action_digest |
| left_record.command.remote_working_directory = str(remote_working_dir) |
| right_record = log_pb2.LogRecord() |
| right_record.remote_metadata.action_digest = right_action_digest |
| right_record.command.remote_working_directory = str(remote_working_dir) |
| |
| inner_left_record = log_pb2.LogRecord() |
| inner_left_record.remote_metadata.action_digest = "22233342341/45" |
| inner_left_record.command.remote_working_directory = str( |
| remote_working_dir) |
| inner_right_record = log_pb2.LogRecord() |
| inner_right_record.remote_metadata.action_digest = "777889112ef/45" |
| inner_right_record.command.remote_working_directory = str( |
| remote_working_dir) |
| inner_left_digest = { |
| str(intermediate): '00abcdef/34' |
| } # is an intermediate output |
| inner_right_digest = {str(intermediate): '00987654/34'} |
| add_output_file_digests_to_record(inner_left_record, inner_left_digest) |
| add_output_file_digests_to_record( |
| inner_right_record, inner_right_digest) |
| |
| left_log_dump = log_pb2.LogDump( |
| records=[left_record, inner_left_record]) |
| right_log_dump = log_pb2.LogDump( |
| records=[inner_right_record, right_record]) |
| left = reproxy_logs.ReproxyLog(left_log_dump) |
| right = reproxy_logs.ReproxyLog(right_log_dump) |
| cfg = dict() |
| diff = action_diff.ActionDiffer(left, right, cfg) |
| |
| # actions for the outer call to trace_artifact() |
| outer_left_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={remote_working_dir / intermediate: "55773311bb987a/282"}, |
| platform=dict(), |
| output_files=dict()) |
| outer_right_action = remotetool.ShowActionResult( |
| command=['twiddle'], |
| inputs={remote_working_dir / intermediate: "bb987aaad663fd/282" |
| }, # different |
| platform=dict(), |
| output_files=dict()) |
| |
| # actions for the inner call to trace_artifact() |
| inner_left_action = remotetool.ShowActionResult( |
| command=['fiddle'], |
| inputs={source: "aa81557311b987a/189"}, |
| platform=dict(), |
| output_files=dict()) |
| inner_right_action = remotetool.ShowActionResult( |
| command=['fiddle'], |
| inputs={source: "87be001aa4efbfd/182"}, # different |
| platform=dict(), |
| output_files=dict()) |
| out = io.StringIO() |
| with contextlib.redirect_stdout(out): |
| with mock.patch.object(remotetool.RemoteTool, 'show_action', |
| side_effect=[ |
| outer_left_action, |
| outer_right_action, |
| inner_left_action, |
| inner_right_action, |
| ]) as mock_show_action: |
| root_causes = list( |
| diff.trace_actions(left_action_digest, right_action_digest)) |
| |
| check_messages = [ |
| # outer level of trace_artifact() |
| f'Remote output file {intermediate} differs', |
| # inner level of trace_artifact() -- root cause |
| f'Input {source} does not come from a remote action', |
| ] |
| |
| for m in check_messages: |
| self.assertIn(m, out.getvalue()) |
| |
| self.assertEqual(len(root_causes), 1) |
| self.assertTrue( |
| any( |
| check_messages[1] in line |
| for line in root_causes[0].explanation)) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |