Skip to content

Commit 6f925e6

Browse files
SCANPY-146 Parse properties from sonar-project.properties file (#155)
1 parent aa91766 commit 6f925e6

File tree

8 files changed

+230
-11
lines changed

8 files changed

+230
-11
lines changed

its/tests/test_dummy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#
2020
import unittest
2121

22+
2223
class DummyTest(unittest.TestCase):
2324
def test_dummy(self):
2425
self.assertTrue(True)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ toml = '>=0.10.2'
3333
requests = "^2.32.3"
3434
responses = "^0.25.6"
3535
pyfakefs = "^5.7.4"
36+
jproperties = "^2.1.2"
3637

3738
[tool.poetry.group]
3839
[tool.poetry.group.dev]

scripts/generate_licenseheaders.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/bin/env bash
1+
#!/bin/bash
22
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
33
pushd "$SCRIPT_DIR/.."
44

src/pysonar_scanner/configuration/__init__.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
# along with this program; if not, write to the Free Software Foundation,
1818
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
#
20+
from pathlib import Path
2021

2122
from pysonar_scanner.configuration import properties
2223
from pysonar_scanner.configuration.cli import CliConfigurationLoader
23-
from pysonar_scanner.configuration.properties import SONAR_TOKEN, Key
24+
from pysonar_scanner.configuration.properties import SONAR_TOKEN, SONAR_PROJECT_BASE_DIR, Key
25+
from pysonar_scanner.configuration import properties, sonar_project_properties
2426

2527
from pysonar_scanner.exceptions import MissingKeyException
2628

@@ -35,11 +37,14 @@ def load() -> dict[Key, any]:
3537
# each property loader is required to return NO default values.
3638
# E.g. if no property has been set, an empty dict must be returned.
3739
# Default values should be set through the get_static_default_properties() method
38-
39-
return {
40-
**get_static_default_properties(),
41-
**CliConfigurationLoader.load(),
42-
}
40+
resolved_properties = get_static_default_properties()
41+
cli_properties = CliConfigurationLoader.load()
42+
# CLI properties have a higher priority than properties file,
43+
# but we need to resolve them first to load the properties file
44+
base_dir = Path(cli_properties.get(SONAR_PROJECT_BASE_DIR, "."))
45+
resolved_properties.update(sonar_project_properties.load(base_dir))
46+
resolved_properties.update(cli_properties)
47+
return resolved_properties
4348

4449

4550
def get_token(config: dict[Key, any]) -> str:

src/pysonar_scanner/configuration/properties.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
SONAR_PROJECT_BASE_DIR: Key = "sonar.projectBaseDir"
5757
SONAR_SCANNER_JAVA_OPTS: Key = "sonar.scanner.javaOpts"
5858
SONAR_PROJECT_KEY: Key = "sonar.projectKey"
59+
SONAR_PROJECT_NAME: Key = "sonar.projectName"
60+
SONAR_SOURCES: Key = "sonar.sources"
61+
SONAR_EXCLUSIONS: Key = "sonar.exclusions"
62+
SONAR_TESTS: Key = "sonar.tests"
5963

6064

6165
@dataclass
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
20+
from pathlib import Path
21+
from typing import Dict
22+
import os
23+
from jproperties import Properties
24+
25+
26+
def load(base_dir: Path) -> Dict[str, str]:
27+
filepath = base_dir / "sonar-project.properties"
28+
if not os.path.isfile(filepath):
29+
return {}
30+
31+
props = Properties()
32+
with open(filepath, "rb") as f:
33+
props.load(f)
34+
35+
return {key: value.data for key, value in props.items() if value.data is not None}

tests/test_configuration.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
# along with this program; if not, write to the Free Software Foundation,
1818
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
#
20-
import unittest
2120

2221
from unittest.mock import patch
2322

24-
from pysonar_scanner.configuration import ConfigurationLoader
23+
import pyfakefs.fake_filesystem_unittest as pyfakefs
24+
2525
from pysonar_scanner import configuration
2626
from pysonar_scanner.configuration.properties import (
2727
SONAR_PROJECT_KEY,
28+
SONAR_PROJECT_NAME,
2829
SONAR_SCANNER_APP,
2930
SONAR_SCANNER_APP_VERSION,
3031
SONAR_SCANNER_BOOTSTRAP_START_TIME,
@@ -34,16 +35,21 @@
3435
SONAR_SCANNER_SKIP_JRE_PROVISIONING,
3536
SONAR_SCANNER_SOCKET_TIMEOUT,
3637
SONAR_SCANNER_TRUSTSTORE_PASSWORD,
38+
SONAR_EXCLUSIONS,
39+
SONAR_SOURCES,
40+
SONAR_TESTS,
3741
SONAR_TOKEN,
3842
SONAR_USER_HOME,
3943
SONAR_VERBOSE,
4044
)
45+
from pysonar_scanner.configuration import ConfigurationLoader, SONAR_PROJECT_BASE_DIR
4146
from pysonar_scanner.exceptions import MissingKeyException
4247

4348

44-
class TestConfigurationLoader(unittest.TestCase):
49+
class TestConfigurationLoader(pyfakefs.TestCase):
4550
def setUp(self):
4651
self.maxDiff = None
52+
self.setUpPyfakefs()
4753

4854
@patch("sys.argv", ["myscript.py", "--token", "myToken", "--sonar-project-key", "myProjectKey"])
4955
def test_defaults(self):
@@ -65,7 +71,7 @@ def test_defaults(self):
6571
}
6672
self.assertDictEqual(configuration, expected_configuration)
6773

68-
@patch("pysonar_scanner.configuration.get_static_default_properties", result={})
74+
@patch("pysonar_scanner.configuration.get_static_default_properties", return_value={})
6975
@patch("sys.argv", ["myscript.py"])
7076
def test_no_defaults_in_configuration_loaders(self, get_static_default_properties_mock):
7177
config = ConfigurationLoader.load()
@@ -77,3 +83,85 @@ def test_get_token(self):
7783

7884
with self.subTest("Token is absent"), self.assertRaises(MissingKeyException):
7985
configuration.get_token({})
86+
87+
@patch("sys.argv", ["myscript.py", "--token", "myToken", "--sonar-project-key", "myProjectKey"])
88+
def test_load_sonar_project_properties(self):
89+
90+
self.fs.create_file(
91+
"sonar-project.properties",
92+
contents=(
93+
"""
94+
sonar.projectKey=overwritten-project-key
95+
sonar.projectName=My Project\n
96+
sonar.sources=src # my sources\n
97+
sonar.exclusions=**/generated/**/*,**/deprecated/**/*,**/testdata/**/*\n
98+
"""
99+
),
100+
)
101+
configuration = ConfigurationLoader.load()
102+
expected_configuration = {
103+
SONAR_TOKEN: "myToken",
104+
SONAR_PROJECT_KEY: "myProjectKey",
105+
SONAR_PROJECT_NAME: "My Project",
106+
SONAR_SOURCES: "src # my sources",
107+
SONAR_EXCLUSIONS: "**/generated/**/*,**/deprecated/**/*,**/testdata/**/*",
108+
SONAR_SCANNER_APP: "python",
109+
SONAR_SCANNER_APP_VERSION: "1.0",
110+
SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME],
111+
SONAR_VERBOSE: False,
112+
SONAR_SCANNER_SKIP_JRE_PROVISIONING: False,
113+
SONAR_USER_HOME: "~/.sonar",
114+
SONAR_SCANNER_CONNECT_TIMEOUT: 5,
115+
SONAR_SCANNER_SOCKET_TIMEOUT: 60,
116+
SONAR_SCANNER_RESPONSE_TIMEOUT: 0,
117+
SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit",
118+
SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit",
119+
}
120+
self.assertDictEqual(configuration, expected_configuration)
121+
122+
@patch(
123+
"sys.argv",
124+
[
125+
"myscript.py",
126+
"--token",
127+
"myToken",
128+
"--sonar-project-key",
129+
"myProjectKey",
130+
"--sonar-project-base-dir",
131+
"custom/path",
132+
],
133+
)
134+
def test_load_sonar_project_properties_from_custom_path(self):
135+
self.fs.create_dir("custom/path")
136+
self.fs.create_file(
137+
"custom/path/sonar-project.properties",
138+
contents=(
139+
"""
140+
sonar.projectKey=custom-path-project-key
141+
sonar.projectName=Custom Path Project
142+
sonar.sources=src/main
143+
sonar.tests=src/test
144+
"""
145+
),
146+
)
147+
configuration = ConfigurationLoader.load()
148+
expected_configuration = {
149+
SONAR_TOKEN: "myToken",
150+
SONAR_PROJECT_KEY: "myProjectKey",
151+
SONAR_PROJECT_NAME: "Custom Path Project",
152+
SONAR_SOURCES: "src/main",
153+
SONAR_PROJECT_BASE_DIR: "custom/path",
154+
SONAR_TESTS: "src/test",
155+
SONAR_SCANNER_APP: "python",
156+
SONAR_SCANNER_APP_VERSION: "1.0",
157+
SONAR_SCANNER_BOOTSTRAP_START_TIME: configuration[SONAR_SCANNER_BOOTSTRAP_START_TIME],
158+
SONAR_VERBOSE: False,
159+
SONAR_SCANNER_SKIP_JRE_PROVISIONING: False,
160+
SONAR_USER_HOME: "~/.sonar",
161+
SONAR_SCANNER_CONNECT_TIMEOUT: 5,
162+
SONAR_SCANNER_SOCKET_TIMEOUT: 60,
163+
SONAR_SCANNER_RESPONSE_TIMEOUT: 0,
164+
SONAR_SCANNER_KEYSTORE_PASSWORD: "changeit",
165+
SONAR_SCANNER_TRUSTSTORE_PASSWORD: "changeit",
166+
}
167+
self.assertDictEqual(configuration, expected_configuration)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
20+
from pathlib import Path
21+
22+
import pyfakefs.fake_filesystem_unittest as pyfakefs
23+
from pysonar_scanner.configuration import sonar_project_properties
24+
25+
26+
class TestPropertiesFile(pyfakefs.TestCase):
27+
def setUp(self):
28+
self.setUpPyfakefs()
29+
30+
def test_load_sonar_project_properties(self):
31+
self.fs.create_file(
32+
"sonar-project.properties",
33+
contents=(
34+
"sonar.projectKey=my-project\n"
35+
"sonar.projectName=My Project\n"
36+
"sonar.sources=src # my sources\n"
37+
"sonar.sources=src\n"
38+
"sonar.exclusions=**/generated/**/*,**/deprecated/**/*,**/testdata/**/*\n"
39+
),
40+
)
41+
properties = sonar_project_properties.load(Path("."))
42+
43+
self.assertEqual(properties.get("sonar.projectKey"), "my-project")
44+
self.assertEqual(properties.get("sonar.projectName"), "My Project")
45+
self.assertEqual(properties.get("sonar.sources"), "src")
46+
self.assertEqual(properties.get("sonar.exclusions"), "**/generated/**/*,**/deprecated/**/*,**/testdata/**/*")
47+
48+
def test_load_sonar_project_properties_custom_path(self):
49+
self.fs.create_file(
50+
"custom/path/sonar-project.properties",
51+
contents=("sonar.projectKey=my-project\n" "sonar.projectName=My Project\n"),
52+
)
53+
properties = sonar_project_properties.load(Path("custom/path"))
54+
55+
self.assertEqual(properties.get("sonar.projectKey"), "my-project")
56+
self.assertEqual(properties.get("sonar.projectName"), "My Project")
57+
58+
def test_load_missing_file(self):
59+
properties = sonar_project_properties.load(Path("."))
60+
self.assertEqual(len(properties), 0)
61+
62+
def test_load_empty_file(self):
63+
self.fs.create_file("sonar-project.properties", contents="")
64+
properties = sonar_project_properties.load(Path("."))
65+
66+
self.assertEqual(len(properties), 0)
67+
68+
def test_load_with_malformed_lines_jproperties(self):
69+
self.fs.create_file(
70+
"sonar-project.properties",
71+
contents=(
72+
"valid.key=valid value\n"
73+
"malformed line without equals\n"
74+
"another.valid.key=another valid value\n"
75+
"=value without key\n"
76+
),
77+
)
78+
79+
properties = sonar_project_properties.load(Path("."))
80+
81+
self.assertEqual(properties.get("valid.key"), "valid value")
82+
self.assertEqual(properties.get("another.valid.key"), "another valid value")
83+
self.assertEqual(properties.get("malformed"), "line without equals")
84+
self.assertEqual(properties.get(""), "value without key")
85+
self.assertEqual(len(properties), 4)

0 commit comments

Comments
 (0)