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