Add a script for diffing command lines based on bazel aquery outputs

Progress towards #5380 and #5883

RELNOTES: None.
PiperOrigin-RevId: 227646729
diff --git a/tools/cmd_line_differ/cmd_line_differ_test.py b/tools/cmd_line_differ/cmd_line_differ_test.py
new file mode 100644
index 0000000..f5f8f2c
--- /dev/null
+++ b/tools/cmd_line_differ/cmd_line_differ_test.py
@@ -0,0 +1,195 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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
+#
+#    http://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.
+
+import unittest
+from src.main.protobuf import analysis_pb2
+from tools.cmd_line_differ import cmd_line_differ
+from third_party.py import mock
+try:
+  # Python 2
+  from cStringIO import StringIO
+except ImportError:
+  # Python 3
+  from io import StringIO
+
+
+def make_aquery_output(actions, artifact_paths):
+  action_graph = analysis_pb2.ActionGraphContainer()
+  for artifact_path in artifact_paths:
+    next_id = len(action_graph.artifacts)
+    artifact = action_graph.artifacts.add()
+    artifact.id = str(next_id)
+    artifact.exec_path = artifact_path
+  for next_action in actions:
+    action = action_graph.actions.add()
+    action.output_ids.extend(next_action["output_ids"])
+    action.arguments.extend(next_action["arguments"])
+  return action_graph
+
+
+class CmdLineDifferTest(unittest.TestCase):
+
+  def test_no_difference(self):
+    action_graph = make_aquery_output(
+        actions=[{
+            "arguments": ["-a", "-b"],
+            "output_ids": ["0", "1"]
+        }, {
+            "arguments": ["-c"],
+            "output_ids": ["2"]
+        }],
+        artifact_paths=["exec/path/zero", "exec/path/one", "exec/path/two"])
+    mock_stdout = StringIO()
+    with mock.patch("sys.stdout", mock_stdout):
+      cmd_line_differ._aquery_diff(action_graph, action_graph)
+      self.assertEqual(mock_stdout.getvalue(), "No difference\n")
+
+  def test_no_difference_different_output_files_order(self):
+    first = make_aquery_output(
+        actions=[
+            {
+                "arguments": ["-a", "-b"],
+                "output_ids": ["0", "1"]
+            },
+        ],
+        artifact_paths=["exec/path/zero", "exec/path/one"])
+    second = make_aquery_output(
+        actions=[
+            {
+                "arguments": ["-a", "-b"],
+                "output_ids": ["1", "0"]
+            },
+        ],
+        artifact_paths=["exec/path/zero", "exec/path/one"])
+
+    mock_stdout = StringIO()
+    with mock.patch("sys.stdout", mock_stdout):
+      cmd_line_differ._aquery_diff(first, second)
+      self.assertEqual(mock_stdout.getvalue(), "No difference\n")
+
+  def test_first_has_extra_output_files(self):
+    first = make_aquery_output(
+        actions=[
+            {
+                "arguments": ["-a", "-b"],
+                "output_ids": ["0", "1"]
+            },
+            {
+                "arguments": ["-c"],
+                "output_ids": ["2"]
+            },
+        ],
+        artifact_paths=["exec/path/zero", "exec/path/one", "exec/path/two"],
+    )
+    second = make_aquery_output(
+        actions=[
+            {
+                "arguments": ["-a", "-b"],
+                "output_ids": ["1", "0"]
+            },
+        ],
+        artifact_paths=["exec/path/zero", "exec/path/one", "exec/path/two"],
+    )
+
+    expected_error = ("Aquery output before change contains an action "
+                      "that generates the following outputs that aquery "
+                      "output after change doesn't:\nexec/path/two\n\n")
+    mock_stdout = StringIO()
+    with mock.patch("sys.stdout", mock_stdout):
+      cmd_line_differ._aquery_diff(first, second)
+      self.assertEqual(mock_stdout.getvalue(), expected_error)
+
+  def test_second_has_extra_output_files(self):
+    first = make_aquery_output(
+        actions=[
+            {
+                "arguments": ["-a", "-b"],
+                "output_ids": ["0", "1"]
+            },
+        ],
+        artifact_paths=["exec/path/zero", "exec/path/one", "exec/path/two"],
+    )
+    second = make_aquery_output(
+        actions=[
+            {
+                "arguments": ["-a", "-b"],
+                "output_ids": ["0", "1"]
+            },
+            {
+                "arguments": ["-c"],
+                "output_ids": ["2"]
+            },
+        ],
+        artifact_paths=["exec/path/zero", "exec/path/one", "exec/path/two"],
+    )
+
+    expected_error = ("Aquery output after change contains an action that"
+                      " generates the following outputs that aquery"
+                      " output before change doesn't:\nexec/path/two\n\n")
+    mock_stdout = StringIO()
+    with mock.patch("sys.stdout", mock_stdout):
+      cmd_line_differ._aquery_diff(first, second)
+      self.assertEqual(mock_stdout.getvalue(), expected_error)
+
+  def test_different_command_lines(self):
+    first = make_aquery_output(
+        actions=[
+            {
+                "arguments": ["-a", "-d"],
+                "output_ids": ["0", "1"]
+            },
+            {
+                "arguments": ["-c"],
+                "output_ids": ["2"]
+            },
+        ],
+        artifact_paths=["exec/path/zero", "exec/path/one", "exec/path/two"],
+    )
+    second = make_aquery_output(
+        actions=[
+            {
+                "arguments": ["-a", "-b"],
+                "output_ids": ["0", "1"]
+            },
+            {
+                "arguments": ["-c", "-d"],
+                "output_ids": ["2"]
+            },
+        ],
+        artifact_paths=["exec/path/zero", "exec/path/one", "exec/path/two"],
+    )
+
+    expected_error_one = "\n".join([
+        "Difference in action that generates the following outputs:",
+        "exec/path/two",
+        "Aquery output before change has the following command line:", "-c",
+        "Aquery output after change has the following command line:", "-c",
+        "-d", "\n"
+    ])
+    expected_error_two = "\n".join([
+        "Difference in action that generates the following outputs:",
+        "exec/path/one", "exec/path/zero",
+        "Aquery output before change has the following command line:", "-a",
+        "-d", "Aquery output after change has the following command line:",
+        "-a", "-b", "\n"
+    ])
+    mock_stdout = StringIO()
+    with mock.patch("sys.stdout", mock_stdout):
+      cmd_line_differ._aquery_diff(first, second)
+      self.assertIn(expected_error_one, mock_stdout.getvalue())
+      self.assertIn(expected_error_two, mock_stdout.getvalue())
+
+
+if __name__ == "__main__":
+  unittest.main()