| #!/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 argparse |
| import contextlib |
| import io |
| import os |
| import re |
| import subprocess |
| import sys |
| import tempfile |
| import unittest |
| from pathlib import Path |
| from unittest import mock |
| |
| import output_leak_scanner |
| |
| from typing import Any, Sequence |
| |
| |
| def _strs(items: Sequence[Any]) -> Sequence[str]: |
| return [str(i) for i in items] |
| |
| |
| def _paths(items: Sequence[Any]) -> Sequence[Path]: |
| return [Path(i) for i in items] |
| |
| |
| def _write_text_file_contents(path: Path, contents: str): |
| with open(path, "w") as f: |
| f.write(contents) |
| |
| |
| def _write_binary_file_contents(path: Path, contents: bytearray): |
| with open(path, "wb") as f: |
| f.write(contents) |
| |
| |
| class PathPatternTests(unittest.TestCase): |
| def test_init(self): |
| path = Path("over/the/rainbow") |
| p = output_leak_scanner.PathPattern(path) |
| self.assertEqual(p.text, str(path)) |
| self.assertTrue(p.re_text.search("somewhere/over/the/rainbow")) |
| self.assertTrue(p.re_bin.search(b"somewhere/over/the/rainbow")) |
| |
| def test_equal(self): |
| # different, but equivalent objects |
| self.assertEqual( |
| output_leak_scanner.PathPattern(Path("foo/bar")), |
| output_leak_scanner.PathPattern(Path("foo/bar")), |
| ) |
| |
| def test_not_equal(self): |
| self.assertNotEqual( |
| output_leak_scanner.PathPattern(Path("foo/bar")), |
| output_leak_scanner.PathPattern(Path("bar/foo")), |
| ) |
| |
| def test_dot_should_never_be_used(self): |
| with self.assertRaises(ValueError): |
| output_leak_scanner.PathPattern(Path()) |
| with self.assertRaises(ValueError): |
| output_leak_scanner.PathPattern(Path(".")) |
| |
| def test_dots_in_paths_are_literal(self): |
| path = Path("build.obj.stuff") |
| p = output_leak_scanner.PathPattern(path) |
| self.assertEqual(p.text, str(path)) |
| self.assertTrue(p.re_text.search("foo/build.obj.stuff/bar")) |
| self.assertFalse(p.re_text.search("foo/build_obj_stuff/bar")) |
| |
| |
| class MainArgParserTests(unittest.TestCase): |
| def test_empty(self): |
| parser = output_leak_scanner._MAIN_ARG_PARSER |
| args = parser.parse_args([]) |
| self.assertIsNone(args.label) |
| self.assertTrue(args.execute) |
| self.assertEqual(args.outputs, []) |
| |
| def test_label(self): |
| label = "//foo:bar" |
| parser = output_leak_scanner._MAIN_ARG_PARSER |
| args = parser.parse_args(["--label", label]) |
| self.assertEqual(args.label, label) |
| |
| def test_execute(self): |
| parser = output_leak_scanner._MAIN_ARG_PARSER |
| args = parser.parse_args(["--execute"]) |
| self.assertTrue(args.execute) |
| |
| def test_no_execute(self): |
| parser = output_leak_scanner._MAIN_ARG_PARSER |
| args = parser.parse_args(["--no-execute"]) |
| self.assertFalse(args.execute) |
| |
| def test_outputs(self): |
| outputs = ["foo/bar.txt", "bar/foo.o"] |
| parser = output_leak_scanner._MAIN_ARG_PARSER |
| args = parser.parse_args(outputs) # positional |
| self.assertEqual(args.outputs, _paths(outputs)) |
| |
| |
| class ErrorMsgTests(unittest.TestCase): |
| def test_no_label(self): |
| s = io.StringIO() |
| with contextlib.redirect_stdout(s): |
| output_leak_scanner.error_msg("oops") |
| self.assertIn("oops", s.getvalue()) |
| |
| def test_label(self): |
| s = io.StringIO() |
| with contextlib.redirect_stdout(s): |
| output_leak_scanner.error_msg("oops", label="LABEL") |
| self.assertIn("LABEL", s.getvalue()) |
| self.assertIn("oops", s.getvalue()) |
| |
| |
| class WholeWordPatternTests(unittest.TestCase): |
| def test_add_boundaries(self): |
| self.assertEqual( |
| output_leak_scanner._whole_word_pattern("zyx"), r"\bzyx\b" |
| ) |
| |
| def test_no_extra_boundaries(self): |
| self.assertEqual( |
| output_leak_scanner._whole_word_pattern(r"\bqwer\b"), r"\bqwer\b" |
| ) |
| |
| |
| class FileContainsSubpathTests(unittest.TestCase): |
| def test_ignore_nonexistent(self): |
| with tempfile.TemporaryDirectory() as td: |
| self.assertFalse( |
| output_leak_scanner.file_contains_subpath( |
| Path(td) / "nonexistent", |
| output_leak_scanner.PathPattern(Path("nonexistent")), |
| ) |
| ) |
| |
| def test_ignore_dir(self): |
| with tempfile.TemporaryDirectory() as td: |
| self.assertFalse( |
| output_leak_scanner.file_contains_subpath( |
| Path(td), output_leak_scanner.PathPattern(Path("other")) |
| ) |
| ) |
| |
| def test_text_no_match(self): |
| with tempfile.TemporaryDirectory() as td: |
| tf = (Path(td) / __name__).with_suffix(".txt") |
| _write_text_file_contents(tf, "a\nb\nc\n") |
| self.assertFalse( |
| output_leak_scanner.file_contains_subpath( |
| tf, output_leak_scanner.PathPattern(Path("def")) |
| ) |
| ) |
| |
| def test_text_match(self): |
| with tempfile.TemporaryDirectory() as td: |
| tf = (Path(td) / __name__).with_suffix(".txt") |
| _write_text_file_contents(tf, "a\nb\nc\n") |
| self.assertTrue( |
| output_leak_scanner.file_contains_subpath( |
| tf, output_leak_scanner.PathPattern(Path("b")) |
| ) |
| ) |
| |
| def test_text_no_match_partial_word(self): |
| with tempfile.TemporaryDirectory() as td: |
| tf = (Path(td) / __name__).with_suffix(".txt") |
| _write_text_file_contents(tf, "ab\nbb\nbc\n") |
| self.assertFalse( |
| output_leak_scanner.file_contains_subpath( |
| tf, output_leak_scanner.PathPattern(Path("b")) |
| ) |
| ) |
| self.assertTrue( |
| output_leak_scanner.file_contains_subpath( |
| tf, output_leak_scanner.PathPattern(Path("bb")) |
| ) |
| ) |
| |
| def test_binary_no_match(self): |
| with tempfile.TemporaryDirectory() as td: |
| tf = (Path(td) / __name__).with_suffix(".txt") |
| _write_binary_file_contents(tf, b"\xcc\n\xdd\xee\n\xff\n+abc+\n") |
| self.assertFalse( |
| output_leak_scanner.file_contains_subpath( |
| tf, output_leak_scanner.PathPattern(Path("pdq")) |
| ) |
| ) |
| |
| def test_binary_match(self): |
| with tempfile.TemporaryDirectory() as td: |
| tf = (Path(td) / __name__).with_suffix(".txt") |
| _write_binary_file_contents(tf, b"\xcc\n\xdd\xee\n\xff\n+abc+\n") |
| self.assertTrue( |
| output_leak_scanner.file_contains_subpath( |
| tf, output_leak_scanner.PathPattern(Path("abc")) |
| ) |
| ) |
| |
| def test_binary_no_match_partial(self): |
| with tempfile.TemporaryDirectory() as td: |
| tf = (Path(td) / __name__).with_suffix(".txt") |
| _write_binary_file_contents(tf, b"\xcc\n\xdd\xee\n\xff\n+abc+\n") |
| self.assertFalse( |
| output_leak_scanner.file_contains_subpath( |
| tf, output_leak_scanner.PathPattern(Path("b")) |
| ) |
| ) |
| |
| |
| class PathsWithBuildDirLeaksTests(unittest.TestCase): |
| def test_negatives(self): |
| build_dir = Path("build/me/here") |
| cases = _paths( |
| [ |
| "build/me/there", |
| "out/default", |
| "rebuild/me/here", |
| "build/me/heretic", |
| ] |
| ) |
| pattern = output_leak_scanner.PathPattern(build_dir) |
| actual = list( |
| output_leak_scanner.paths_with_build_dir_leaks( |
| cases, pattern.re_text |
| ) |
| ) |
| self.assertEqual(actual, []) |
| |
| def test_positives(self): |
| build_dir = Path("build/me/here") |
| cases = _paths( |
| ["/tmp/build/me/here", "build/me/here", "build/me/here/foo/bar.txt"] |
| ) |
| pattern = output_leak_scanner.PathPattern(build_dir) |
| actual = set( |
| output_leak_scanner.paths_with_build_dir_leaks( |
| cases, pattern.re_text |
| ) |
| ) |
| self.assertEqual(actual, set(cases)) |
| |
| |
| class TokensWithBuildDirLeaks(unittest.TestCase): |
| def test_negatives(self): |
| build_dir = Path("build/here") |
| cases = [ |
| "-f", |
| "build/not/here", |
| "out/default", |
| "rebuild/here", |
| "build/heretic", |
| ] |
| pattern = output_leak_scanner.PathPattern(build_dir) |
| actual = list( |
| output_leak_scanner.tokens_with_build_dir_leaks( |
| cases, pattern.re_text |
| ) |
| ) |
| self.assertEqual(actual, []) |
| |
| def test_positives(self): |
| build_dir = Path("build/here") |
| cases = ["/work/out/build/here", "build/here", "build/here/out.txt"] |
| pattern = output_leak_scanner.PathPattern(build_dir) |
| actual = list( |
| output_leak_scanner.tokens_with_build_dir_leaks( |
| cases, pattern.re_text |
| ) |
| ) |
| self.assertEqual(actual, cases) |
| |
| def test_exceptions(self): |
| build_dir = Path("build/here") |
| cases = [ |
| f"-fdebug-prefix-map=/some/where/{build_dir}", |
| f"-ffile-prefix-map=/some/where/{build_dir}/foo", |
| f"-fmacro-prefix-map=/some/where/{build_dir}/bar", |
| f"-fcoverage-prefix-map=/some/where/{build_dir}", |
| ] |
| pattern = output_leak_scanner.PathPattern(build_dir) |
| actual = list( |
| output_leak_scanner.tokens_with_build_dir_leaks( |
| cases, pattern.re_text |
| ) |
| ) |
| self.assertEqual(actual, []) |
| |
| |
| class PreflightChecksTests(unittest.TestCase): |
| def test_no_findings(self): |
| build_dir_pattern = output_leak_scanner.PathPattern(Path("any")) |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| return_code = output_leak_scanner.preflight_checks( |
| paths=[], command=[], pattern=build_dir_pattern |
| ) |
| self.assertEqual(return_code, 0) |
| self.assertEqual(stdout.getvalue(), "") # quiet |
| |
| def test_path_leak(self): |
| build_dir = Path("as/df") |
| build_dir_pattern = output_leak_scanner.PathPattern(build_dir) |
| stdout = io.StringIO() |
| bad = build_dir / "g.h" |
| with contextlib.redirect_stdout(stdout): |
| return_code = output_leak_scanner.preflight_checks( |
| paths=[bad], command=[], pattern=build_dir_pattern |
| ) |
| self.assertEqual(return_code, 1) |
| message = stdout.getvalue() |
| self.assertIn("Error", message) |
| self.assertIn(str(bad), message) |
| |
| def test_command_leak(self): |
| build_dir = Path("zs/df") |
| build_dir_pattern = output_leak_scanner.PathPattern(build_dir) |
| stdout = io.StringIO() |
| bad = build_dir / "g.h" |
| with contextlib.redirect_stdout(stdout): |
| return_code = output_leak_scanner.preflight_checks( |
| paths=[], |
| command=_strs(["touch", bad]), |
| pattern=build_dir_pattern, |
| ) |
| self.assertEqual(return_code, 1) |
| message = stdout.getvalue() |
| self.assertIn("Error", message) |
| self.assertIn(str(bad), message) |
| |
| |
| class PostflightChecksTests(unittest.TestCase): |
| def test_no_findings(self): |
| build_dir_pattern = output_leak_scanner.PathPattern(Path("out/f/b")) |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| output_leak_scanner, "file_contains_subpath", return_value=False |
| ) as mock_search: |
| return_code = output_leak_scanner.postflight_checks( |
| outputs=["foo.o"], subpath=build_dir_pattern |
| ) |
| self.assertEqual(return_code, 0) |
| mock_search.assert_called_once() |
| self.assertEqual(stdout.getvalue(), "") |
| |
| def test_has_findings(self): |
| build_dir = Path("out/f/b") |
| build_dir_pattern = output_leak_scanner.PathPattern(build_dir) |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| output_leak_scanner, "file_contains_subpath", return_value=True |
| ) as mock_search: |
| return_code = output_leak_scanner.postflight_checks( |
| outputs=["foo.o"], subpath=build_dir_pattern |
| ) |
| self.assertEqual(return_code, 1) |
| mock_search.assert_called_once() |
| message = stdout.getvalue() |
| self.assertIn("Error", message) |
| self.assertIn(str(build_dir), message) |
| |
| |
| class ScanLeaksTests(unittest.TestCase): |
| def test_missing_ddash(self): |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| # --help will trigger call to sys.exit, which we |
| # want to avoid in testing. |
| with mock.patch.object(sys, "exit") as mock_exit: |
| return_code = output_leak_scanner.scan_leaks( |
| [], |
| exec_root=Path("/home"), |
| working_dir=Path("/home/build"), |
| ) |
| mock_exit.assert_called_once() |
| self.assertEqual(return_code, 1) |
| message = stdout.getvalue() |
| self.assertIn("Error", message) |
| self.assertIn("'--' is missing", message) |
| |
| def test_skip_checking_subdir_dot(self): |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| output_leak_scanner, "preflight_checks", return_value=0 |
| ) as mock_pre_check: |
| with mock.patch.object( |
| output_leak_scanner, "postflight_checks", return_value=0 |
| ) as mock_post_check: |
| with mock.patch.object( |
| subprocess, "call", return_value=0 |
| ) as mock_call: |
| return_code = output_leak_scanner.scan_leaks( |
| ["--", "touch", "down.txt"], |
| exec_root=Path("/home/project"), |
| working_dir=Path("/home/project"), |
| ) |
| mock_pre_check.assert_not_called() |
| mock_post_check.assert_not_called() |
| mock_call.assert_called_once() |
| self.assertEqual(return_code, 0) |
| message = stdout.getvalue() |
| self.assertEqual(message, "") |
| |
| def test_no_execute(self): |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| output_leak_scanner, "preflight_checks", return_value=0 |
| ) as mock_pre_check: |
| with mock.patch.object( |
| subprocess, "call", return_value=1 |
| ) as mock_call: |
| return_code = output_leak_scanner.scan_leaks( |
| ["--no-execute", "--", "touch", "down.txt"], |
| exec_root=Path("/home"), |
| working_dir=Path("/home/build"), |
| ) |
| mock_pre_check.assert_called_once() |
| mock_call.assert_not_called() |
| self.assertEqual(return_code, 0) |
| message = stdout.getvalue() |
| self.assertEqual(message, "") |
| |
| def test_execute_command_error(self): |
| stdout = io.StringIO() |
| output = "down.txt" |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| subprocess, "call", return_value=2 |
| ) as mock_call: |
| return_code = output_leak_scanner.scan_leaks( |
| [output, "--", "touch", output], |
| exec_root=Path("/home"), |
| working_dir=Path("/home/build"), |
| ) |
| mock_call.assert_called_once() |
| self.assertEqual(return_code, 2) |
| message = stdout.getvalue() |
| self.assertEqual(message, "") |
| |
| def test_execute_success_preflight_error(self): |
| stdout = io.StringIO() |
| output = "down.txt" |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| output_leak_scanner, "preflight_checks", return_value=1 |
| ) as mock_pre_check: |
| with mock.patch.object( |
| output_leak_scanner, "postflight_checks", return_value=0 |
| ) as mock_post_check: |
| with mock.patch.object( |
| subprocess, "call", return_value=0 |
| ) as mock_call: |
| return_code = output_leak_scanner.scan_leaks( |
| [output, "--", "touch", output], |
| exec_root=Path("/home"), |
| working_dir=Path("/home/build"), |
| ) |
| mock_pre_check.assert_called_once() |
| mock_call.assert_called_once() |
| mock_post_check.assert_called_once() |
| self.assertEqual(return_code, 1) |
| |
| def test_execute_success_postflight_error(self): |
| stdout = io.StringIO() |
| output = Path("down.txt") |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| output_leak_scanner, "preflight_checks", return_value=0 |
| ) as mock_pre_check: |
| with mock.patch.object( |
| output_leak_scanner, "postflight_checks", return_value=1 |
| ) as mock_post_check: |
| with mock.patch.object( |
| subprocess, "call", return_value=0 |
| ) as mock_call: |
| return_code = output_leak_scanner.scan_leaks( |
| _strs([output, "--", "touch", output]), |
| exec_root=Path("/home"), |
| working_dir=Path("/home/build"), |
| ) |
| mock_pre_check.assert_called_once() |
| mock_call.assert_called_once() |
| mock_post_check.assert_called_once() |
| self.assertEqual(return_code, 1) |
| |
| def test_execute_success_scans_clean(self): |
| stdout = io.StringIO() |
| output = Path("down.txt") |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| output_leak_scanner, "preflight_checks", return_value=0 |
| ) as mock_pre_check: |
| with mock.patch.object( |
| output_leak_scanner, "postflight_checks", return_value=0 |
| ) as mock_post_check: |
| with mock.patch.object( |
| subprocess, "call", return_value=0 |
| ) as mock_call: |
| return_code = output_leak_scanner.scan_leaks( |
| _strs([output, "--", "touch", output]), |
| exec_root=Path("/home"), |
| working_dir=Path("/home/build"), |
| ) |
| mock_pre_check.assert_called_once() |
| mock_call.assert_called_once() |
| mock_post_check.assert_called_once() |
| self.assertEqual(return_code, 0) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |