Skip to content

Commit ad8fbe8

Browse files
SCANPY-136 Support custom analyzer properties (#171)
1 parent 040ef7e commit ad8fbe8

File tree

7 files changed

+129
-6
lines changed

7 files changed

+129
-6
lines changed

src/pysonar_scanner/configuration/cli.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,40 @@
2020
import argparse
2121

2222
from pysonar_scanner.configuration import properties
23+
from pysonar_scanner.exceptions import UnexpectedCliArgument
2324

2425

2526
class CliConfigurationLoader:
2627

2728
@classmethod
2829
def load(cls) -> dict[str, any]:
29-
args = cls.__parse_cli_args()
30+
args, unknown_args = cls.__parse_cli_args()
3031
config = {}
3132
for prop in properties.PROPERTIES:
3233
if prop.cli_getter is not None:
3334
value = prop.cli_getter(args)
3435
config[prop.name] = value
3536

37+
# Handle unknown args starting with '-D'
38+
for arg in unknown_args:
39+
if not arg.startswith("-D"):
40+
raise UnexpectedCliArgument(f"Unexpected argument: {arg}")
41+
key_value = arg[2:].split("=", 1)
42+
if len(key_value) == 2:
43+
key, value = key_value
44+
config[key] = value
45+
else:
46+
# If no value is provided, set the key to "true"
47+
config[arg[2:]] = "true"
48+
3649
return {k: v for k, v in config.items() if v is not None}
3750

3851
@classmethod
39-
def __parse_cli_args(cls) -> argparse.Namespace:
40-
parser = argparse.ArgumentParser(description="Sonar scanner CLI for Python")
52+
def __parse_cli_args(cls) -> tuple[argparse.Namespace, list[str]]:
53+
parser = argparse.ArgumentParser(
54+
description="Sonar scanner CLI for Python",
55+
epilog="Analysis properties not listed here will also be accepted, as long as they start with the -D prefix.",
56+
)
4157

4258
parser.add_argument(
4359
"-t",
@@ -439,4 +455,4 @@ def __parse_cli_args(cls) -> argparse.Namespace:
439455
"--sonar-modules", "-Dsonar.modules", type=str, help="Comma-delimited list of modules to analyze"
440456
)
441457

442-
return parser.parse_args()
458+
return parser.parse_known_args()

src/pysonar_scanner/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ class ChecksumException(Exception):
3939
pass
4040

4141

42+
class UnexpectedCliArgument(Exception):
43+
pass
44+
45+
4246
class JreProvisioningException(Exception):
4347
pass
4448

tests/its/test_minimal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ def test_minimal_project(sonarqube_client: SonarQubeClient, cli: CliClient):
4141

4242
def test_minimal_project_unexpected_arg(cli: CliClient):
4343
process = cli.run_analysis(params=["-unexpected"], sources_dir="minimal")
44-
assert process.returncode == 2, str(process.stdout)
45-
assert "error: unrecognized arguments: -unexpected" in process.stdout
44+
assert process.returncode == 1, str(process.stdout)
45+
assert "Unexpected argument: -unexpected" in process.stdout
4646

4747

4848
def test_invalid_token(sonarqube_client: SonarQubeClient, cli: CliClient):

tests/unit/test_configuration_cli.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
SONAR_PYTHON_XUNIT_SKIP_DETAILS,
8888
SONAR_MODULES,
8989
)
90+
from pysonar_scanner.exceptions import UnexpectedCliArgument
9091

9192
EXPECTED_CONFIGURATION = {
9293
SONAR_TOKEN: "myToken",
@@ -439,3 +440,82 @@ def test_both_boolean_args_given(self):
439440
with patch("sys.argv", [*patch_template, "-Dsonar.scm.exclusions.disabled=true"]):
440441
configuration = CliConfigurationLoader.load()
441442
self.assertTrue(configuration.get(SONAR_SCM_EXCLUSIONS_DISABLED))
443+
444+
@patch(
445+
"sys.argv",
446+
[
447+
"myscript.py",
448+
"--token",
449+
"myToken",
450+
"--sonar-project-key",
451+
"myProjectKey",
452+
"-Dunknown.property=unknownValue",
453+
"-Danother.unknown.property=anotherValue",
454+
],
455+
)
456+
def test_unknown_args_with_D_prefix(self):
457+
configuration = CliConfigurationLoader.load()
458+
expected_configuration = {
459+
SONAR_TOKEN: "myToken",
460+
SONAR_PROJECT_KEY: "myProjectKey",
461+
"unknown.property": "unknownValue",
462+
"another.unknown.property": "anotherValue",
463+
}
464+
self.assertDictEqual(configuration, expected_configuration)
465+
466+
@patch(
467+
"sys.argv",
468+
[
469+
"myscript.py",
470+
"--token",
471+
"myToken",
472+
"--sonar-project-key",
473+
"myProjectKey",
474+
"-Dunknown.flag",
475+
],
476+
)
477+
def test_unknown_args_no_value(self):
478+
configuration = CliConfigurationLoader.load()
479+
expected_configuration = {
480+
SONAR_TOKEN: "myToken",
481+
SONAR_PROJECT_KEY: "myProjectKey",
482+
"unknown.flag": "true",
483+
}
484+
self.assertDictEqual(configuration, expected_configuration)
485+
486+
@patch(
487+
"sys.argv",
488+
[
489+
"myscript.py",
490+
"--token",
491+
"myToken",
492+
"--sonar-project-key",
493+
"myProjectKey",
494+
"-unknown.property=some_value",
495+
],
496+
)
497+
def test_unknown_args_missing_D_prefix(self):
498+
with self.assertRaises(
499+
UnexpectedCliArgument, msg="Unexpected argument format: -unknown.property=some_value=another_value"
500+
):
501+
CliConfigurationLoader.load()
502+
503+
@patch(
504+
"sys.argv",
505+
[
506+
"myscript.py",
507+
"--token",
508+
"myToken",
509+
"--sonar-project-key",
510+
"myProjectKey",
511+
"-Dsonar.unknown.property=some_value=another_value",
512+
],
513+
)
514+
def test_unknown_args_unexpected_format(self):
515+
configuration = CliConfigurationLoader.load()
516+
expected_configuration = {
517+
SONAR_TOKEN: "myToken",
518+
SONAR_PROJECT_KEY: "myProjectKey",
519+
"sonar.unknown.property": "some_value=another_value",
520+
}
521+
self.assertDictEqual(configuration, expected_configuration)

tests/unit/test_configuration_loader.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,22 @@ def test_properties_priority(self):
375375
# CLI args have highest priority
376376
self.assertEqual(configuration[SONAR_PROJECT_KEY], "ProjectKeyFromCLI")
377377
self.assertEqual(configuration[SONAR_TOKEN], "myToken") # CLI overrides env var
378+
379+
@patch(
380+
"sys.argv",
381+
[
382+
"myscript.py",
383+
"--token",
384+
"myToken",
385+
"--sonar-project-key",
386+
"myProjectKey",
387+
"-Dunknown.property=unknownValue",
388+
"-Danother.unknown.property=anotherValue",
389+
],
390+
)
391+
def test_unknown_args_with_D_prefix(self):
392+
configuration = ConfigurationLoader.load()
393+
self.assertEqual(configuration["unknown.property"], "unknownValue")
394+
self.assertEqual(configuration["another.unknown.property"], "anotherValue")
395+
self.assertEqual(configuration[SONAR_TOKEN], "myToken")
396+
self.assertEqual(configuration[SONAR_PROJECT_KEY], "myProjectKey")

tests/unit/test_pyproject_toml.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def test_load_toml_file_with_sonarqube_config(self):
3636
projectName = "My Project"
3737
sources = "src"
3838
exclusions = "**/generated/**/*,**/deprecated/**/*,**/testdata/**/*"
39+
some.unknownProperty = "unknown_property_value"
3940
""",
4041
)
4142
properties = TomlConfigurationLoader.load(Path("."))
@@ -46,6 +47,7 @@ def test_load_toml_file_with_sonarqube_config(self):
4647
self.assertEqual(
4748
properties.sonar_properties.get("sonar.exclusions"), "**/generated/**/*,**/deprecated/**/*,**/testdata/**/*"
4849
)
50+
self.assertEqual(properties.sonar_properties.get("sonar.some.unknownProperty"), "unknown_property_value")
4951

5052
def test_load_toml_file_kebab_case(self):
5153
self.fs.create_file(

tests/unit/test_sonar_project_properties.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def test_load_sonar_project_properties(self):
3636
"sonar.sources=src # my sources\n"
3737
"sonar.sources=src\n"
3838
"sonar.exclusions=**/generated/**/*,**/deprecated/**/*,**/testdata/**/*\n"
39+
"sonar.some.unknownProperty=my_unknown_property_value\n"
3940
),
4041
)
4142
properties = sonar_project_properties.load(Path("."))
@@ -44,6 +45,7 @@ def test_load_sonar_project_properties(self):
4445
self.assertEqual(properties.get("sonar.projectName"), "My Project")
4546
self.assertEqual(properties.get("sonar.sources"), "src")
4647
self.assertEqual(properties.get("sonar.exclusions"), "**/generated/**/*,**/deprecated/**/*,**/testdata/**/*")
48+
self.assertEqual(properties.get("sonar.some.unknownProperty"), "my_unknown_property_value")
4749

4850
def test_load_sonar_project_properties_custom_path(self):
4951
self.fs.create_file(

0 commit comments

Comments
 (0)