diff --git a/components/collector/src/source_collectors/__init__.py b/components/collector/src/source_collectors/__init__.py index dd9fd7cbb4..e355f75b41 100644 --- a/components/collector/src/source_collectors/__init__.py +++ b/components/collector/src/source_collectors/__init__.py @@ -1,6 +1,6 @@ """Metric collectors per source.""" -from .api_source_collectors.azure_devops import AzureDevopsIssues, AzureDevopsReadyUserStoryPoints +from .api_source_collectors.azure_devops import AzureDevopsIssues, AzureDevopsUserStoryPoints from .api_source_collectors.cxsast import CxSASTSecurityWarnings, CxSASTSourceUpToDateness from .api_source_collectors.gitlab import ( GitLabFailedJobs, GitLabSourceUpToDateness, GitLabUnmergedBranches, GitLabUnusedJobs) @@ -9,7 +9,7 @@ from .api_source_collectors.jenkins import JenkinsFailedJobs, JenkinsJobs from .api_source_collectors.jenkins_test_report import JenkinsTestReportSourceUpToDateness, JenkinsTestReportTests from .api_source_collectors.jira import ( - JiraIssues, JiraManualTestDuration, JiraManualTestExecution, JiraReadyUserStoryPoints) + JiraIssues, JiraManualTestDuration, JiraManualTestExecution, JiraUserStoryPoints) from .api_source_collectors.owasp_dependency_check_jenkins_plugin import ( OWASPDependencyCheckJenkinsPluginSecurityWarnings, OWASPDependencyCheckJenkinsPluginSourceUpToDateness) from .api_source_collectors.quality_time import QualityTimeMetrics, QualityTimeSourceUpToDateness diff --git a/components/collector/src/source_collectors/api_source_collectors/azure_devops.py b/components/collector/src/source_collectors/api_source_collectors/azure_devops.py index 4f15cc3233..49422f474d 100644 --- a/components/collector/src/source_collectors/api_source_collectors/azure_devops.py +++ b/components/collector/src/source_collectors/api_source_collectors/azure_devops.py @@ -60,8 +60,8 @@ async def _work_items(responses: SourceResponses): return (await responses[1].json())["value"] if len(responses) > 1 else [] -class AzureDevopsReadyUserStoryPoints(AzureDevopsIssues): - """Collector to get ready user story points from Azure Devops Server.""" +class AzureDevopsUserStoryPoints(AzureDevopsIssues): + """Collector to get user story points from Azure Devops Server.""" async def _parse_source_responses(self, responses: SourceResponses) -> SourceMeasurement: measurement = await super()._parse_source_responses(responses) diff --git a/components/collector/src/source_collectors/api_source_collectors/jira.py b/components/collector/src/source_collectors/api_source_collectors/jira.py index 65f4d44e18..afa66d62f9 100644 --- a/components/collector/src/source_collectors/api_source_collectors/jira.py +++ b/components/collector/src/source_collectors/api_source_collectors/jira.py @@ -136,8 +136,8 @@ def __value_of_field_to_sum(self, issue: Dict) -> Optional[float]: return value if value is None else float(value) -class JiraReadyUserStoryPoints(JiraFieldSumBase): - """Collector to get ready user story points from Jira.""" +class JiraUserStoryPoints(JiraFieldSumBase): + """Collector to get user story points from Jira.""" field_parameter = "story_points_field" entity_key = "points" diff --git a/components/collector/tests/source_collectors/api_source_collectors/test_azure_devops.py b/components/collector/tests/source_collectors/api_source_collectors/test_azure_devops.py index da519fed7a..6f619e01f8 100644 --- a/components/collector/tests/source_collectors/api_source_collectors/test_azure_devops.py +++ b/components/collector/tests/source_collectors/api_source_collectors/test_azure_devops.py @@ -52,12 +52,12 @@ async def test_issues(self): url=self.work_item_url)]) -class AzureDevopsReadyStoryPointsTest(AzureDevopsTestCase): - """Unit tests for the Azure Devops Server ready story points metric.""" +class AzureDevopsStoryPointsTest(AzureDevopsTestCase): + """Unit tests for the Azure Devops Server story points metric.""" def setUp(self): super().setUp() - self.metric = dict(type="ready_user_story_points", sources=self.sources, addition="sum") + self.metric = dict(type="user_story_points", sources=self.sources, addition="sum") async def test_story_points(self): """Test that the number of story points are returned.""" diff --git a/components/collector/tests/source_collectors/api_source_collectors/test_jira.py b/components/collector/tests/source_collectors/api_source_collectors/test_jira.py index a8e2bdd589..5f06425f2e 100644 --- a/components/collector/tests/source_collectors/api_source_collectors/test_jira.py +++ b/components/collector/tests/source_collectors/api_source_collectors/test_jira.py @@ -90,10 +90,10 @@ async def test_nr_of_test_cases_with_field_name(self): self.entity(key="1", last_test_date=str(parse(self.ten_days_ago).date()), desired_test_frequency="5")]) -class JiraReadyUserStoryPointsTest(JiraTestCase): - """Unit tests for the Jira ready story points collector.""" +class JiraUserStoryPointsTest(JiraTestCase): + """Unit tests for the Jira story points collector.""" - METRIC_TYPE = "ready_user_story_points" + METRIC_TYPE = "user_story_points" async def test_nr_story_points(self): """Test that the number of story points is returned.""" diff --git a/components/server/src/data/datamodel.json b/components/server/src/data/datamodel.json index 7dae719f25..4aa0264875 100644 --- a/components/server/src/data/datamodel.json +++ b/components/server/src/data/datamodel.json @@ -352,29 +352,6 @@ "performance" ] }, - "ready_user_story_points": { - "name": "Ready user story points", - "description": "The number of points of user stories that are ready to implement.", - "scales": [ - "count" - ], - "default_scale": "count", - "unit": "user story points", - "addition": "sum", - "direction": ">", - "target": "100", - "near_target": "75", - "default_source": "azure_devops", - "sources": [ - "azure_devops", - "jira", - "manual_number", - "random" - ], - "tags": [ - "process efficiency" - ] - }, "remediation_effort": { "name": "Violation remediation effort", "description": "The amount of effort it takes to remediate violations.", @@ -665,6 +642,29 @@ "ci" ] }, + "user_story_points": { + "name": "User story points", + "description": "The total number of points of a selection of user stories.", + "scales": [ + "count" + ], + "default_scale": "count", + "unit": "user story points", + "addition": "sum", + "direction": ">", + "target": "100", + "near_target": "75", + "default_source": "azure_devops", + "sources": [ + "azure_devops", + "jira", + "manual_number", + "random" + ], + "tags": [ + "process efficiency" + ] + }, "violations": { "name": "Violations", "description": "The number of violations of programming rules in the software.", @@ -1047,7 +1047,7 @@ "metrics": [ "failed_jobs", "issues", - "ready_user_story_points", + "user_story_points", "source_up_to_dateness", "tests", "unmerged_branches", @@ -1064,7 +1064,7 @@ "metrics": [ "failed_jobs", "issues", - "ready_user_story_points", + "user_story_points", "source_up_to_dateness", "tests", "unmerged_branches", @@ -1080,7 +1080,7 @@ "default_value": "", "metrics": [ "issues", - "ready_user_story_points" + "user_story_points" ] }, "file_path": { @@ -1402,7 +1402,7 @@ } ] }, - "ready_user_story_points": { + "user_story_points": { "name": "user story", "name_plural": "user stories", "measured_attribute": "story_points", @@ -2574,7 +2574,7 @@ "issues", "manual_test_duration", "manual_test_execution", - "ready_user_story_points" + "user_story_points" ] }, "jql": { @@ -2588,7 +2588,7 @@ "issues", "manual_test_duration", "manual_test_execution", - "ready_user_story_points" + "user_story_points" ] }, "username": { @@ -2601,7 +2601,7 @@ "issues", "manual_test_duration", "manual_test_execution", - "ready_user_story_points" + "user_story_points" ] }, "password": { @@ -2614,7 +2614,7 @@ "issues", "manual_test_duration", "manual_test_execution", - "ready_user_story_points" + "user_story_points" ] }, "manual_test_duration_field": { @@ -2660,7 +2660,7 @@ "mandatory": true, "default_value": "Story Points", "metrics": [ - "ready_user_story_points" + "user_story_points" ] } }, @@ -2736,7 +2736,7 @@ } ] }, - "ready_user_story_points": { + "user_story_points": { "name": "user story", "name_plural": "user stories", "measured_attribute": "points", @@ -2917,7 +2917,7 @@ "metrics", "performancetest_duration", "performancetest_stability", - "ready_user_story_points", + "user_story_points", "scalability", "slow_transactions", "security_warnings", @@ -4039,6 +4039,7 @@ "Tests", "Unmerged branches", "Unused CI-jobs", + "User story points", "Violations" ], "api_values": { @@ -4068,6 +4069,7 @@ "Tests": "tests", "Unmerged branches": "unmerged_branches", "Unused CI-jobs": "unused_jobs", + "User story points": "user_story_points", "Violations": "violations", "Violation remediation effort": "remediation_effort" }, @@ -5438,7 +5440,7 @@ "issues", "manual_test_duration", "manual_test_execution", - "ready_user_story_points", + "user_story_points", "source_up_to_dateness", "unmerged_branches" ] @@ -5468,7 +5470,7 @@ "long_units", "performancetest_duration", "performancetest_stability", - "ready_user_story_points", + "user_story_points", "scalability", "security_warnings", "slow_transactions", diff --git a/components/server/src/initialization/database.py b/components/server/src/initialization/database.py index 8a6f35d5f4..207ca7f6e0 100644 --- a/components/server/src/initialization/database.py +++ b/components/server/src/initialization/database.py @@ -11,6 +11,7 @@ # For some reason the init_database() function gets reported as partially uncovered by the feature tests. Ignore. + def init_database() -> Database: # pragma: no cover-behave """Initialize the database connection and contents.""" database_url = os.environ.get("DATABASE_URL", "mongodb://root:root@localhost:27017") @@ -25,6 +26,7 @@ def init_database() -> Database: # pragma: no cover-behave if os.environ.get("LOAD_EXAMPLE_REPORTS", "True").lower() == "true": import_example_reports(database) add_last_flag_to_reports(database) + rename_ready_user_story_points_metric(database) return database @@ -44,3 +46,22 @@ def add_last_flag_to_reports(database: Database) -> None: filter={"report_uuid": report_uuid}, sort=[("timestamp", pymongo.DESCENDING)]) report_ids.append(report["_id"]) database.reports.update_many({"_id": {"$in": report_ids}}, {"$set": {"last": True}}) + + +def rename_ready_user_story_points_metric(database: Database) -> None: # pragma: no cover-behave + """Rename the ready_user_story_points metric to user_story_points.""" + # Introduced when the most recent version of Quality-time was 3.3.0. + reports = list(database.reports.find({"last": True, "deleted": {"$exists": False}})) + for report in reports: + changed = False + for subject in report["subjects"].values(): + for metric in subject["metrics"].values(): + if metric["type"] == "ready_user_story_points": + metric["type"] = "user_story_points" + changed = True + if not metric.get("name"): + metric["name"] = "Ready user story points" + if changed: + report_id = report["_id"] + del report["_id"] + database.reports.replace_one({"_id": report_id}, report) diff --git a/components/server/tests/data/test_datamodel.py b/components/server/tests/data/test_datamodel.py index 251eec58c8..ed08998d17 100644 --- a/components/server/tests/data/test_datamodel.py +++ b/components/server/tests/data/test_datamodel.py @@ -245,11 +245,13 @@ def test_quality_time_source_type_parameter(self): def test_quality_time_metric_type_parameter(self): """Test that the metric type parameter of the Quality-time source lists all metric types.""" all_metric_names = {metric["name"] for metric in self.data_model["metrics"].values()} + all_metric_names.add("Ready user story points") # Removed in first non-patch version after Quality-time v3.3.0 quality_time_metric_names = set( self.data_model["sources"]["quality_time"]["parameters"]["metric_type"]["values"]) self.assertEqual(all_metric_names, quality_time_metric_names) all_metric_api_values = { (metric["name"], metric_id) for metric_id, metric in self.data_model["metrics"].items()} + all_metric_api_values.add(("Ready user story points", "ready_user_story_points")) quality_time_api_values = set( self.data_model["sources"]["quality_time"]["parameters"]["metric_type"]["api_values"].items()) self.assertEqual(all_metric_api_values, quality_time_api_values) diff --git a/components/server/tests/initialization/test_database.py b/components/server/tests/initialization/test_database.py index c4c0a25df6..3dff24109a 100644 --- a/components/server/tests/initialization/test_database.py +++ b/components/server/tests/initialization/test_database.py @@ -66,3 +66,23 @@ def test_add_last_flag_to_reports(self): self.database.reports.find_one.return_value = {"_id": "1"} self.init_database("{}") self.database.reports.update_many.assert_called_once() + + def test_rename_ready_user_story_points(self): + """Test that the ready user story points metric is correctly renamed.""" + self.database.reports.find.return_value = [ + {"_id": "1", "subjects": {}}, + {"_id": "2", "subjects": {"subject1": {"metrics": {}}}}, + {"_id": "3", "subjects": { + "subject2": { + "metrics": { + "metric1": {"type": "violations"}, + "metric2": {"type": "ready_user_story_points"}, + "metric3": {"type": "ready_user_story_points", "name": "Don't change the name"}}}}}] + self.init_database("{}") + self.database.reports.replace_one.assert_called_once_with( + {"_id": "3"}, + {"subjects": { + "subject2": { + "metrics": {"metric1": {"type": "violations"}, + "metric2": {"type": "user_story_points", "name": "Ready user story points"}, + "metric3": {"type": "user_story_points", "name": "Don't change the name"}}}}}) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 244d8ec78d..d7b9915258 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. +## [Unreleased] + +### Changed + +- Rename the 'ready user story points' to 'user story points' as it can not just be used to count ready user stories, but rather any selection of user stories. Closes [#1415](https://github.com/ICTU/quality-time/issues/1415). + ## [3.3.0] - [2020-08-29] ### Fixed diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index f57948dc0d..c5a6791540 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -18,7 +18,6 @@ | Metrics | The amount of metrics from one more quality reports, with specific states and/or tags. | ≦ 0 metrics | | Quality-time | | Performancetest duration | The duration of the performancetest in minutes | ≧ 30 minutes | performance | Performancetest-runner | | Performancetest stability | The duration of the performancetest at which throughput or error count increases. | ≧ 100% of the minutes | performance | Performancetest-runner | -| Ready user story points | The number of points of user stories that are ready to implement. | ≧ 100 user story points | process efficiency | Azure DevOps Server, Jira | | Scalability | The percentage of (max) users at which ramp-up of throughput breaks. | ≧ 75% of the users | performance | Performancetest-runner | | Security warnings | The number of security warnings about the software. | ≦ 0 security warnings | security | Anchore, Bandit, Checkmarx CxSAST, OWASP Dependency Check, OWASP Dependency Check Jenkins plugin, OWASP ZAP, OpenVAS, Pyupio Safety, Snyk, SonarQube | | Size (LOC) | The size of the software in lines of code. | ≦ 30000 lines | maintainability | SonarQube, cloc | @@ -30,6 +29,7 @@ | Tests | The number of tests. | ≧ 0 tests | test quality | Azure DevOps Server, JUnit XML report, Jenkins test report, Performancetest-runner, Robot Framework, SonarQube | | Unmerged branches | The number of branches that have not been merged to the default branch. | ≦ 0 branches | ci | Azure DevOps Server, GitLab | | Unused CI-jobs | The number of continuous integration jobs that are unused. | ≦ 0 CI-jobs | ci | Azure DevOps Server, GitLab, Jenkins | +| User story points | The total number of points of a selection of user stories. | ≧ 100 user story points | process efficiency | Azure DevOps Server, Jira | | Violation remediation effort | The amount of effort it takes to remediate violations. | ≦ 60 minutes | maintainability | SonarQube | | Violations | The number of violations of programming rules in the software. | ≦ 0 violations | maintainability | OJAudit, SonarQube | @@ -39,7 +39,7 @@ | :--- | :---------- | :------ | | [Anchore](https://docs.anchore.com/current/docs/using/integration/ci_cd/inline_scanning/) | Anchore image scan analysis report in JSON format | Source up-to-dateness, Security warnings | | [Axe CSV](https://github.com/ICTU/axe-reports) | An Axe accessibility report in CSV format. | Accessibility violations | -| [Azure DevOps Server](https://azure.microsoft.com/en-us/services/devops/server/) | Azure DevOps Server (formerly known as Team Foundation Server) by Microsoft provides source code management, reporting, requirements management, project management, automated builds, testing and release management. | Failed CI-jobs, Issues, Ready user story points, Source up-to-dateness, Tests, Unmerged branches, Unused CI-jobs | +| [Azure DevOps Server](https://azure.microsoft.com/en-us/services/devops/server/) | Azure DevOps Server (formerly known as Team Foundation Server) by Microsoft provides source code management, reporting, requirements management, project management, automated builds, testing and release management. | Failed CI-jobs, Issues, Source up-to-dateness, Tests, Unmerged branches, Unused CI-jobs, User story points | | [Bandit](https://github.com/PyCQA/bandit) | Bandit is a tool designed to find common security issues in Python code. | Source up-to-dateness, Security warnings | | Calendar date | Warn when the date is too long ago. Can be used to, for example, warn when it is time for the next security test. | Source up-to-dateness | | [Checkmarx CxSAST](https://www.checkmarx.com/products/static-application-security-testing/) | Static analysis software to identify security vulnerabilities in both custom code and open source components. | Source up-to-dateness, Security warnings | @@ -51,7 +51,7 @@ | [JaCoCo Jenkins plugin](https://plugins.jenkins.io/jacoco) | A Jenkins job with a JaCoCo coverage report produced by the JaCoCo Jenkins plugin. | Source up-to-dateness, Test branch coverage, Test line coverage | | [Jenkins](https://jenkins.io/) | Jenkins is an open source continuous integration/continuous deployment server. | Failed CI-jobs, Unused CI-jobs | | [Jenkins test report](https://plugins.jenkins.io/junit) | A Jenkins job with test results. | Source up-to-dateness, Tests | -| [Jira](https://www.atlassian.com/software/jira) | Jira is a proprietary issue tracker developed by Atlassian supporting bug tracking and agile project management. | Issues, Manual test duration, Manual test execution, Ready user story points | +| [Jira](https://www.atlassian.com/software/jira) | Jira is a proprietary issue tracker developed by Atlassian supporting bug tracking and agile project management. | Issues, Manual test duration, Manual test execution, User story points | | Manual number | A manual number. | ¹ | | [NCover](https://www.ncover.com/) | A .NET code coverage solution | Source up-to-dateness, Test branch coverage, Test line coverage | | [OJAudit](https://www.oracle.com/technetwork/developer-tools/jdev) | An Oracle JDeveloper program to audit Java code against JDeveloper's audit rules. | Violations | @@ -320,24 +320,6 @@ | URL to a Performancetest-runner HTML report or a zip with Performancetest-runner HTML reports | URL | Yes | | | Username for basic authentication | String | No | | -### Ready user story points from Azure DevOps Server - -| Parameter | Type | Mandatory | Help | -| :-------- | :--- | :-------- | :--- | -| Issue query in WIQL (Work Item Query Language) | String | Yes | [https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops) | -| Private token | Password | No | [https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops) | -| URL including organization and project | URL | Yes | URL of the Azure DevOps instance, with port if necessary, and with organization and project. For example: 'https://dev.azure.com/{organization}/{project}'. | - -### Ready user story points from Jira - -| Parameter | Type | Mandatory | Help | -| :-------- | :--- | :-------- | :--- | -| Issue query in JQL (Jira Query Language) | String | Yes | [https://confluence.atlassian.com/jirasoftwarecloud/advanced-searching-764478330.html](https://confluence.atlassian.com/jirasoftwarecloud/advanced-searching-764478330.html) | -| Password for basic authentication | Password | No | | -| Story points field (name or id) | String | Yes | [https://confluence.atlassian.com/jirakb/how-to-find-id-for-custom-field-s-744522503.html](https://confluence.atlassian.com/jirakb/how-to-find-id-for-custom-field-s-744522503.html) | -| URL | URL | Yes | URL of the Jira instance, with port if necessary. For example, 'https://jira.example.org'. | -| Username for basic authentication | String | No | | - ### Violation remediation effort from SonarQube | Parameter | Type | Mandatory | Help | @@ -892,6 +874,24 @@ | URL | URL | Yes | URL of the Jenkins instance, with port if necessary, but without path. For example, 'https://jenkins.example.org'. | | Username for basic authentication | String | No | | +### User story points from Azure DevOps Server + +| Parameter | Type | Mandatory | Help | +| :-------- | :--- | :-------- | :--- | +| Issue query in WIQL (Work Item Query Language) | String | Yes | [https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops) | +| Private token | Password | No | [https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops) | +| URL including organization and project | URL | Yes | URL of the Azure DevOps instance, with port if necessary, and with organization and project. For example: 'https://dev.azure.com/{organization}/{project}'. | + +### User story points from Jira + +| Parameter | Type | Mandatory | Help | +| :-------- | :--- | :-------- | :--- | +| Issue query in JQL (Jira Query Language) | String | Yes | [https://confluence.atlassian.com/jirasoftwarecloud/advanced-searching-764478330.html](https://confluence.atlassian.com/jirasoftwarecloud/advanced-searching-764478330.html) | +| Password for basic authentication | Password | No | | +| Story points field (name or id) | String | Yes | [https://confluence.atlassian.com/jirakb/how-to-find-id-for-custom-field-s-744522503.html](https://confluence.atlassian.com/jirakb/how-to-find-id-for-custom-field-s-744522503.html) | +| URL | URL | Yes | URL of the Jira instance, with port if necessary. For example, 'https://jira.example.org'. | +| Username for basic authentication | String | No | | + ### Violations from OJAudit | Parameter | Type | Mandatory | Help | diff --git a/tests/features/measurement.feature b/tests/features/measurement.feature index c2c8942ed5..ab14e33ee3 100644 --- a/tests/features/measurement.feature +++ b/tests/features/measurement.feature @@ -84,7 +84,7 @@ Feature: measurement Then the metric has two measurements Scenario: mark an entity as false positive - Given an existing metric with type "ready_user_story_points" + Given an existing metric with type "user_story_points" Given an existing source with type "azure_devops" When the collector measures "120" | key | story_points | @@ -100,7 +100,7 @@ Feature: measurement Then the metric status is "target_not_met" Scenario: an entity marked as false positive disappears on the next measurement - Given an existing metric with type "ready_user_story_points" + Given an existing metric with type "user_story_points" Given an existing source with type "azure_devops" When the collector measures "120" | key | story_points |