| ## @file | |
| # Unit tests for workspace macro expansion. | |
| # | |
| # Copyright (c) Microsoft Corporation. | |
| # SPDX-License-Identifier: BSD-2-Clause-Patent | |
| # | |
| """Regression tests for workspace macro expansion.""" | |
| import os | |
| import tempfile | |
| import unittest | |
| from unittest import mock | |
| from Common.MultipleWorkspace import MultipleWorkspace | |
| class TestMultipleWorkspace(unittest.TestCase): | |
| """Check workspace resolution without changing compiler option formatting.""" | |
| def setUp(self) -> None: | |
| """Create isolated roots and restore shared workspace state after each test.""" | |
| temporary = tempfile.TemporaryDirectory(prefix="multiple workspace ") | |
| self.addCleanup(temporary.cleanup) | |
| self.workspace = os.path.join(temporary.name, "workspace") | |
| self.packages = [ | |
| os.path.join(temporary.name, "first package"), | |
| os.path.join(temporary.name, "last package"), | |
| ] | |
| for directory in [self.workspace] + self.packages: | |
| os.mkdir(directory) | |
| self.addCleanup(setattr, MultipleWorkspace, "WORKSPACE", MultipleWorkspace.WORKSPACE) | |
| self.addCleanup(setattr, MultipleWorkspace, "PACKAGES_PATH", MultipleWorkspace.PACKAGES_PATH) | |
| MultipleWorkspace.setWs(self.workspace, os.pathsep.join(self.packages)) | |
| def create_file(self, root: str, relative: str) -> str: | |
| """Create an empty file under a test root and return its path.""" | |
| path = os.path.join(root, relative) | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| with open(path, "w"): | |
| pass | |
| return path | |
| def test_workspace_takes_precedence(self) -> None: | |
| """Prefer an existing workspace file regardless of option quoting.""" | |
| relative = "Config/Settings.txt" | |
| self.create_file(self.workspace, relative) | |
| self.create_file(self.packages[0], relative) | |
| for quote in ("", '"', "'"): | |
| with self.subTest(quote=quote): | |
| option = "--config=" + quote + "$(WORKSPACE)/" + relative + quote | |
| self.assertEqual( | |
| option.replace("$(WORKSPACE)", self.workspace), | |
| MultipleWorkspace.handleWsMacro(option), | |
| ) | |
| def test_quoted_paths_with_spaces(self) -> None: | |
| """Resolve quoted paths with spaces while retaining their original quoting.""" | |
| relative = "Config/Build Settings.txt" | |
| self.create_file(self.packages[0], relative) | |
| for option in ( | |
| '"$(WORKSPACE)/Config/Build Settings.txt"', | |
| '--config="$(WORKSPACE)/Config/Build Settings.txt"', | |
| "--config='$(WORKSPACE)/Config/Build Settings.txt'", | |
| '"--config=$(WORKSPACE)/Config/Build Settings.txt"', | |
| "'--config=$(WORKSPACE)/Config/Build Settings.txt'", | |
| '--config="$(WORKSPACE)"/Config/"Build Settings.txt"', | |
| ): | |
| with self.subTest(option=option): | |
| self.assertEqual( | |
| option.replace("$(WORKSPACE)", self.packages[0]), | |
| MultipleWorkspace.handleWsMacro(option), | |
| ) | |
| def test_opposite_quote_inside_quoted_path(self) -> None: | |
| """Preserve a literal apostrophe inside a double-quoted filename.""" | |
| relative = "Config/Developer's Settings.txt" | |
| self.create_file(self.packages[0], relative) | |
| option = '--config="$(WORKSPACE)/' + relative + '"' | |
| self.assertEqual( | |
| option.replace("$(WORKSPACE)", self.packages[0]), | |
| MultipleWorkspace.handleWsMacro(option), | |
| ) | |
| def test_first_existing_package_path_wins(self) -> None: | |
| """Prefer the first package root containing an unquoted path.""" | |
| relative = "Include/Header.h" | |
| for directory in self.packages: | |
| self.create_file(directory, relative) | |
| option = "-I$(WORKSPACE)/" + relative | |
| self.assertEqual( | |
| option.replace("$(WORKSPACE)", self.packages[0]), | |
| MultipleWorkspace.handleWsMacro(option), | |
| ) | |
| def test_later_package_path_is_searched(self) -> None: | |
| """Search later package roots when earlier roots lack the file.""" | |
| relative = "Include/Header.h" | |
| self.create_file(self.packages[1], relative) | |
| option = '-I"$(WORKSPACE)/' + relative + '"' | |
| self.assertEqual( | |
| option.replace("$(WORKSPACE)", self.packages[1]), | |
| MultipleWorkspace.handleWsMacro(option), | |
| ) | |
| def test_missing_path_stays_workspace_relative(self) -> None: | |
| """Keep missing paths rooted in the workspace rather than a package root.""" | |
| for option in ("-I$(WORKSPACE)/Missing", '-I"$(WORKSPACE)/Missing Directory"'): | |
| with self.subTest(option=option): | |
| self.assertEqual( | |
| option.replace("$(WORKSPACE)", self.workspace), | |
| MultipleWorkspace.handleWsMacro(option), | |
| ) | |
| def test_no_package_paths(self) -> None: | |
| """Expand the workspace macro when no package roots are configured.""" | |
| MultipleWorkspace.setWs(self.workspace) | |
| option = '"$(WORKSPACE)/Missing Directory"' | |
| self.assertEqual( | |
| option.replace("$(WORKSPACE)", self.workspace), | |
| MultipleWorkspace.handleWsMacro(option), | |
| ) | |
| def test_no_macro_is_unchanged(self) -> None: | |
| """Leave strings without a workspace macro unchanged.""" | |
| for option in ("", ' -O2\t-DNAME="a b" ', "$(OTHER)/Include"): | |
| with self.subTest(option=option): | |
| self.assertEqual(option, MultipleWorkspace.handleWsMacro(option)) | |
| def test_multiple_options_preserve_whitespace_and_quotes(self) -> None: | |
| """Resolve each option independently without changing surrounding whitespace.""" | |
| self.create_file(self.workspace, "Config/Settings.txt") | |
| self.create_file(self.packages[0], "Include/Header.h") | |
| option = ' -O2\t--config="$(WORKSPACE)/Config/Settings.txt" -I$(WORKSPACE)/Include/Header.h\n' | |
| expected = ( | |
| ' -O2\t--config="' | |
| + self.workspace | |
| + '/Config/Settings.txt" -I' | |
| + self.packages[0] | |
| + "/Include/Header.h\n" | |
| ) | |
| self.assertEqual(expected, MultipleWorkspace.handleWsMacro(option)) | |
| def test_windows_backslashes_are_preserved(self) -> None: | |
| """Check Windows-style paths without treating backslashes as escapes.""" | |
| MultipleWorkspace.WORKSPACE = r"C:\Main Workspace" | |
| MultipleWorkspace.PACKAGES_PATH = [r"D:\First Package", r"E:\Last Package"] | |
| option = r'/I"$(WORKSPACE)\Include Files\Header.h"' | |
| path = r"D:\First Package\Include Files\Header.h" | |
| with mock.patch("Common.LongFilePathOs.path.exists", side_effect=lambda candidate: candidate == path) as exists: | |
| self.assertEqual( | |
| option.replace("$(WORKSPACE)", MultipleWorkspace.PACKAGES_PATH[0]), | |
| MultipleWorkspace.handleWsMacro(option), | |
| ) | |
| self.assertEqual( | |
| [ | |
| mock.call(r"C:\Main Workspace\Include Files\Header.h"), | |
| mock.call(path), | |
| ], | |
| exists.call_args_list, | |
| ) | |
| def TheTestSuite() -> unittest.TestSuite: | |
| """Return the regression suite for the BaseTools test runner.""" | |
| return unittest.defaultTestLoader.loadTestsFromTestCase(TestMultipleWorkspace) | |
| if __name__ == "__main__": | |
| unittest.main() |