diff --git a/CHANGELOG.md b/CHANGELOG.md index f221bc8cee..0cdb9223dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- Change initial project validation to take into account non GxP requirement([#1006](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1006)) - Make IS_GXP property available for CFTP documents ([#996](https://github.com/opendevstack/ods-jenkins-shared-library/issues/996)) - Use a consistent notion of document version and remove header ([#987](https://github.com/opendevstack/ods-jenkins-shared-library/issues/987)) - Improve Document Generation Experience ([#991](https://github.com/opendevstack/ods-jenkins-shared-library/issues/991)) diff --git a/src/org/ods/orchestration/usecase/JiraUseCase.groovy b/src/org/ods/orchestration/usecase/JiraUseCase.groovy index 1f9f2995af..811c5eb802 100644 --- a/src/org/ods/orchestration/usecase/JiraUseCase.groovy +++ b/src/org/ods/orchestration/usecase/JiraUseCase.groovy @@ -380,6 +380,7 @@ class JiraUseCase { def releaseStatusIssueKey = this.project.buildParams.releaseStatusJiraIssueKey if (message) { String commentToAdd = "${message}\n\nSee: ${this.steps.env.RUN_DISPLAY_URL}" + commentToAdd += "\n\nPlease note that for a successful Deploy to D, the above-mentioned issues need to be in status Done." logger.debug("Adding comment to Jira issue with key ${releaseStatusIssueKey}: ${commentToAdd}") this.jira.appendCommentToIssue(releaseStatusIssueKey, commentToAdd) logger.info("Comment was added to Jira issue with key ${releaseStatusIssueKey}: ${commentToAdd}") diff --git a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy index d26963e7f5..50ad872a5b 100644 --- a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy +++ b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy @@ -50,7 +50,7 @@ import java.time.LocalDateTime 'PublicMethodsBeforeNonPublicMethods']) class LeVADocumentUseCase extends DocGenUseCase { - protected static final boolean IS_GXP_PROJECT_DEFAULT = true + private static final String NOT_MANDATORY_CONTENT = 'Not mandatory.' protected static Map DOCUMENT_TYPE_NAMES = [ (DocumentType.CSD as String) : 'Combined Specification Document', @@ -1000,20 +1000,14 @@ class LeVADocumentUseCase extends DocGenUseCase { sections: sections, documentHistory: docHistory?.getDocGenFormat() ?: [], documentHistoryLatestVersionId: docHistory?.latestVersionId ?: 1, - isGxpProject: isGxpProject(this.project.getProjectProperties()), + isGxpProject: this.project.isGxpProject(), ] ] - def uri = this.createDocument(documentType, null, data_, [:], null, getDocumentTemplateName(documentType), watermarkText) this.updateJiraDocumentationTrackingIssue(documentType, uri, docHistory?.getVersion() as String) return uri } - protected boolean isGxpProject(Map projectProperties) { - String isGxp = projectProperties."PROJECT.IS_GXP" - return isGxp != null ? isGxp.toBoolean() : IS_GXP_PROJECT_DEFAULT - } - @NonCPS private def computeKeysInDocForTIP(def data) { return data.collect { it.key } @@ -1528,7 +1522,7 @@ class LeVADocumentUseCase extends DocGenUseCase { date_created : LocalDateTime.now().toString(), buildParameter: this.project.buildParams, git : repo ? repo.data.git : this.project.gitData, - gxp : isGxpProject(this.project.getProjectProperties()), + gxp : project.isGxpProject(), openShift : [apiUrl: this.project.getOpenShiftApiUrl()], jenkins : [ buildNumber: this.steps.env.BUILD_NUMBER, @@ -1693,10 +1687,14 @@ class LeVADocumentUseCase extends DocGenUseCase { throw new RuntimeException("Error: unable to create ${documentType}. " + 'Could not obtain document chapter data from Jira.') } - // Extract-out the section, as needed for the DocGen interface - return sections.collectEntries { sec -> - [(sec.section): sec + [content: this.convertImages(sec.content)]] + + def sectionCollection = sections.collectEntries { sec -> + [(sec.section): sec + [content: this.project.replaceIssueContentWithNonMandatoryText(sec) ? + '

' + this.NOT_MANDATORY_CONTENT + '

' : this.convertImages(sec.content)]] } + + // Extract-out the section, as needed for the DocGen interface + return sectionCollection } protected Map getDocumentSectionsFileOptional(String documentType) { diff --git a/src/org/ods/orchestration/util/MROPipelineUtil.groovy b/src/org/ods/orchestration/util/MROPipelineUtil.groovy index b0f07ed146..48bbe2711f 100644 --- a/src/org/ods/orchestration/util/MROPipelineUtil.groovy +++ b/src/org/ods/orchestration/util/MROPipelineUtil.groovy @@ -10,7 +10,6 @@ import org.ods.orchestration.dependency.DependencyGraph import org.ods.orchestration.dependency.Node import org.ods.services.OpenShiftService import org.ods.services.ServiceRegistry -import org.ods.orchestration.util.DeploymentDescriptor import org.ods.orchestration.phases.DeployOdsComponent import org.ods.orchestration.phases.FinalizeOdsComponent import org.ods.orchestration.phases.FinalizeNonOdsComponent @@ -21,6 +20,7 @@ import org.yaml.snakeyaml.Yaml class MROPipelineUtil extends PipelineUtil { class PipelineConfig { + // TODO: deprecate .pipeline-config.yml in favor of release-manager.yml static final List FILE_NAMES = ["release-manager.yml", ".pipeline-config.yml"] @@ -36,23 +36,27 @@ class MROPipelineUtil extends PipelineUtil { static final List PHASE_EXECUTOR_TYPES = [ PHASE_EXECUTOR_TYPE_MAKEFILE, - PHASE_EXECUTOR_TYPE_SHELLSCRIPT + PHASE_EXECUTOR_TYPE_SHELLSCRIPT, ] static final List INSTALLABLE_REPO_TYPES = [ REPO_TYPE_ODS_CODE as String, REPO_TYPE_ODS_SERVICE as String, - REPO_TYPE_ODS_INFRA as String + REPO_TYPE_ODS_INFRA as String, ] + } class PipelineEnvs { + static final String DEV = "dev" static final String QA = "qa" static final String PROD = "prod" + } class PipelinePhases { + static final String BUILD = "Build" static final String DEPLOY = "Deploy" static final String FINALIZE = "Finalize" @@ -61,6 +65,7 @@ class MROPipelineUtil extends PipelineUtil { static final String TEST = "Test" static final List ALWAYS_PARALLEL = [] + } static final String COMPONENT_METADATA_FILE_NAME = 'metadata.yml' @@ -88,46 +93,6 @@ class MROPipelineUtil extends PipelineUtil { } } - private void executeODSComponent(Map repo, String baseDir, boolean failfast = true, - String jenkinsFile = 'Jenkinsfile') { - this.steps.dir(baseDir) { - if (repo.data.openshift.resurrectedBuild) { - logger.info("Repository '${repo.id}' is in sync with OpenShift, no need to rebuild") - return - } - def job - def env = [] - env.addAll(this.project.getMainReleaseManagerEnv()) - this.project.buildParams.each { key, value -> - env << "BUILD_PARAM_${key.toUpperCase()}=${value}" - } - env << "NOTIFY_BB_BUILD=${!project.isWorkInProgress}" - this.steps.withEnv (env) { - job = this.loadGroovySourceFile("${baseDir}/${jenkinsFile}") - } - // Collect ODS build artifacts for repo. - // We get a map with at least two keys ("build" and "deployments"). - def buildArtifacts = job.getBuildArtifactURIs() - buildArtifacts.each { k, v -> - if (k != 'failedStage') { - repo.data.openshift[k] = v - } - } - def versionAndBuild = "${this.project.buildParams.version}/${this.steps.env.BUILD_NUMBER}" - repo.data.openshift[DeploymentDescriptor.CREATED_BY_BUILD_STR] = versionAndBuild - this.logger.debug("Collected ODS build artifacts for repo '${repo.id}': ${repo.data.openshift}") - - if (buildArtifacts.failedStage) { - repo.data << ['failedStage': buildArtifacts.failedStage] - if (failfast) { - throw new RuntimeException("Error: aborting due to previous errors in repo '${repo.id}'.") - } else { - this.logger.warn("Got errors in repo '${repo.id}', will fail delayed.") - } - } - } - } - Map loadPipelineConfig(String path, Map repo) { if (!path?.trim()) { throw new IllegalArgumentException("Error: unable to parse pipeline config. 'path' is undefined.") @@ -226,7 +191,7 @@ class MROPipelineUtil extends PipelineUtil { repo.id, { checkoutNotReleaseManagerRepo(repo, recheckout) - } + }, ] } @@ -265,7 +230,7 @@ class MROPipelineUtil extends PipelineUtil { previousSucessfulCommit: lastSuccessCommit, url: scm.GIT_URL, baseTag: this.project.baseTag, - targetTag: this.project.targetTag + targetTag: this.project.targetTag, ] def repoPath = "${this.steps.env.WORKSPACE}/${REPOS_BASE_DIR}/${repo.id}" loadPipelineConfig(repoPath, repo) @@ -283,66 +248,6 @@ class MROPipelineUtil extends PipelineUtil { } } - private Map checkOutNotReleaseManagerRepoInNotPromotionMode(Map repo, boolean isWorkInProgress) { - Map scmResult = [ : ] - String gitReleaseBranch = this.project.gitReleaseBranch - if ("master" == gitReleaseBranch) { - gitReleaseBranch = repo.branch - } - - // check if release manager repo already has a release branch - if (git.remoteBranchExists(gitReleaseBranch)) { - try { - scmResult.scm = checkoutBranchInRepoDir(repo, gitReleaseBranch) - scmResult.scmBranch = gitReleaseBranch - } catch (ex) { - if (! isWorkInProgress) { - this.logger.warn """ - Checkout of '${gitReleaseBranch}' for repo '${repo.id}' failed. - Attempting to checkout '${repo.branch}' and create the release branch from it. - """ - // Possible reasons why this might happen: - // * Release branch manually created in RM repo - // * Repo is added to metadata.yml file on a release branch - // * Release branch has been deleted in repo - - scmResult.scm = createBranchFromDefaultBranch(repo, gitReleaseBranch) - scmResult.scmBranch = gitReleaseBranch - } else { - this.logger.warn """ - Checkout of '${gitReleaseBranch}' for repo '${repo.id}' failed. - Attempting to checkout branch '${repo.branch}'. - """ - scmResult.scm = checkoutBranchInRepoDir(repo, repo.branch) - scmResult.scmBranch = repo.branch - } - } - } else { - if (! isWorkInProgress) { - scmResult.scm = createBranchFromDefaultBranch(repo, gitReleaseBranch) - scmResult.scmBranch = gitReleaseBranch - } else { - this.logger.info("Since in WIP and no release branch exists (${this.project.gitReleaseBranch}), checking out branch ${repo.branch} for repo ${repo.id}") - scmResult.scm = checkoutBranchInRepoDir(repo, repo.branch) - scmResult.scmBranch = repo.branch - } - } - return scmResult - } - - private def createBranchFromDefaultBranch(Map repo, String branchName) { - this.logger.info("Creating branch ${branchName} from branch ${repo.branch} for repo ${repo.id} ") - def scm = checkoutBranchInRepoDir(repo, repo.branch) - if (repo.branch != branchName) { - steps.dir("${REPOS_BASE_DIR}/${repo.id}") { - git.checkoutNewLocalBranch(branchName) - } - } else { - this.logger.info("No need to create branch ${branchName} for repo ${repo.id} ") - } - return scm - } - def checkoutTagInRepoDir(Map repo, String tag) { this.logger.info("Checkout tag ${repo.id}@${tag}") def credentialsId = this.project.services.bitbucket.credentials.id @@ -460,7 +365,7 @@ class MROPipelineUtil extends PipelineUtil { postExecute(this.steps, repo) } } - } + }, ] } @@ -497,6 +402,106 @@ class MROPipelineUtil extends PipelineUtil { } } + private void executeODSComponent(Map repo, String baseDir, boolean failfast = true, + String jenkinsFile = 'Jenkinsfile') { + this.steps.dir(baseDir) { + if (repo.data.openshift.resurrectedBuild) { + logger.info("Repository '${repo.id}' is in sync with OpenShift, no need to rebuild") + return + } + def job + def env = [] + env.addAll(this.project.getMainReleaseManagerEnv()) + this.project.buildParams.each { key, value -> + env << "BUILD_PARAM_${key.toUpperCase()}=${value}" + } + env << "NOTIFY_BB_BUILD=${!project.isWorkInProgress}" + this.steps.withEnv (env) { + job = this.loadGroovySourceFile("${baseDir}/${jenkinsFile}") + } + // Collect ODS build artifacts for repo. + // We get a map with at least two keys ("build" and "deployments"). + def buildArtifacts = job.getBuildArtifactURIs() + buildArtifacts.each { k, v -> + if (k != 'failedStage') { + repo.data.openshift[k] = v + } + } + def versionAndBuild = "${this.project.buildParams.version}/${this.steps.env.BUILD_NUMBER}" + repo.data.openshift[DeploymentDescriptor.CREATED_BY_BUILD_STR] = versionAndBuild + this.logger.debug("Collected ODS build artifacts for repo '${repo.id}': ${repo.data.openshift}") + + if (buildArtifacts.failedStage) { + repo.data << ['failedStage': buildArtifacts.failedStage] + if (failfast) { + throw new RuntimeException("Error: aborting due to previous errors in repo '${repo.id}'.") + } else { + this.logger.warn("Got errors in repo '${repo.id}', will fail delayed.") + } + } + } + } + + private Map checkOutNotReleaseManagerRepoInNotPromotionMode(Map repo, boolean isWorkInProgress) { + Map scmResult = [ : ] + String gitReleaseBranch = this.project.gitReleaseBranch + if ("master" == gitReleaseBranch) { + gitReleaseBranch = repo.branch + } + + // check if release manager repo already has a release branch + if (git.remoteBranchExists(gitReleaseBranch)) { + try { + scmResult.scm = checkoutBranchInRepoDir(repo, gitReleaseBranch) + scmResult.scmBranch = gitReleaseBranch + } catch (ex) { + if (! isWorkInProgress) { + this.logger.warn """ + Checkout of '${gitReleaseBranch}' for repo '${repo.id}' failed. + Attempting to checkout '${repo.branch}' and create the release branch from it. + """ + // Possible reasons why this might happen: + // * Release branch manually created in RM repo + // * Repo is added to metadata.yml file on a release branch + // * Release branch has been deleted in repo + + scmResult.scm = createBranchFromDefaultBranch(repo, gitReleaseBranch) + scmResult.scmBranch = gitReleaseBranch + } else { + this.logger.warn """ + Checkout of '${gitReleaseBranch}' for repo '${repo.id}' failed. + Attempting to checkout branch '${repo.branch}'. + """ + scmResult.scm = checkoutBranchInRepoDir(repo, repo.branch) + scmResult.scmBranch = repo.branch + } + } + } else { + if (! isWorkInProgress) { + scmResult.scm = createBranchFromDefaultBranch(repo, gitReleaseBranch) + scmResult.scmBranch = gitReleaseBranch + } else { + this.logger.info("Since in WIP and no release branch exists (${this.project.gitReleaseBranch}), checking out branch ${repo.branch} for repo ${repo.id}") + scmResult.scm = checkoutBranchInRepoDir(repo, repo.branch) + scmResult.scmBranch = repo.branch + } + } + return scmResult + } + + private createBranchFromDefaultBranch(Map repo, String branchName) { + this.logger.info("Creating branch ${branchName} from branch ${repo.branch} for repo ${repo.id} ") + def scm = checkoutBranchInRepoDir(repo, repo.branch) + if (repo.branch != branchName) { + steps.dir("${REPOS_BASE_DIR}/${repo.id}") { + git.checkoutNewLocalBranch(branchName) + } + } else { + this.logger.info("No need to create branch ${branchName} for repo ${repo.id} ") + } + return scm + } + private boolean isRepoModified(Map repo) { if (!repo.data.envStateCommit) { logger.debug("Last recorded commit of '${repo.id}' cannot be retrieved.") @@ -583,7 +588,7 @@ class MROPipelineUtil extends PipelineUtil { OpenShiftService.DEPLOYMENTCONFIG_KIND, deploymentDescriptor.deploymentNames ) - } catch(ex) { + } catch (ex) { logger.info( "Resurrection of previous build for '${repo.id}' not possible as " + "not all deployments could be retrieved: ${ex.message}" @@ -613,4 +618,5 @@ class MROPipelineUtil extends PipelineUtil { repo.data.openshift.deployments = deployments logger.debug("Data from previous Jenkins build:\r${repo.data.openshift}") } + } diff --git a/src/org/ods/orchestration/util/Project.groovy b/src/org/ods/orchestration/util/Project.groovy index 3ffcfac169..26852066b3 100644 --- a/src/org/ods/orchestration/util/Project.groovy +++ b/src/org/ods/orchestration/util/Project.groovy @@ -27,7 +27,9 @@ import java.nio.file.Paths 'PublicMethodsBeforeNonPublicMethods']) class Project { + static final String IS_GXP_PROJECT_PROPERTY = 'PROJECT.IS_GXP' static final String DEFAULT_TEMPLATE_VERSION = '1.2' + static final boolean IS_GXP_PROJECT_DEFAULT = true class JiraDataItem implements Map, Serializable { static final String TYPE_BUGS = 'bugs' @@ -64,16 +66,17 @@ class Project { TYPE_DOCS, ] - static final List TYPES_TO_BE_CLOSED = [ + static final List COMMON_TYPES_TO_BE_CLOSED = [ + TYPE_BUGS, TYPE_EPICS, TYPE_MITIGATIONS, TYPE_REQUIREMENTS, TYPE_RISKS, TYPE_TECHSPECS, TYPE_TESTS, - TYPE_DOCS, ] + static final String ISSUE_STATUS_TODO = 'to do' static final String ISSUE_STATUS_DONE = 'done' static final String ISSUE_STATUS_CANCELLED = 'cancelled' @@ -373,7 +376,7 @@ class Project { this.logger.warn "WIP_Jira_Issues: ${this.data.jira.undone}" String message = ProjectMessagesUtil.generateWIPIssuesMessage(this) - if(!this.isWorkInProgress){ + if (!this.isWorkInProgress){ throw new OpenIssuesException(message) } @@ -381,7 +384,7 @@ class Project { this.addCommentInReleaseStatus(message) } - if(this.jiraUseCase.jira) { + if (this.jiraUseCase.jira) { logger.debug("Verify that each unit test in Jira project ${this.key} has exactly one component assigned.") def faultMap = [:] this.data.jira.tests @@ -391,7 +394,7 @@ class Project { faultMap.put(entry.key, entry.value.get("components").size()) } } - if(faultMap.size() != 0) { + if (faultMap.size() != 0) { def faultyTestIssues = faultMap.keySet() .collect { key -> key + ": " + faultMap.get(key) + "; " } .inject("") { temp, val -> temp + val } @@ -419,23 +422,12 @@ class Project { return !values.isEmpty() } - @NonCPS - boolean isProjectReadyToFreeze(Map data) { - def result = true - JiraDataItem.TYPES_TO_BE_CLOSED.each { type -> - if (data.containsKey(type)) { - result = result & (data[type].find { k, v -> issueIsWIP(v) } == null) - } - } - return result - } - @NonCPS protected Map computeWipJiraIssues(Map data) { - def result = [:] + Map result = [:] JiraDataItem.TYPES_WITH_STATUS.each { type -> if (data.containsKey(type)) { - result[type] = data[type].findAll { k, v -> issueIsWIP(v) }.keySet() as List + result[type] = data[type].findAll { k, v -> issueIsWIPandMandatory(v, type) }.keySet() as List } } return result @@ -449,9 +441,11 @@ class Project { */ @NonCPS protected Map computeWipDocChapterPerDocument(Map data) { - (data[JiraDataItem.TYPE_DOCS] ?: [:]) + Map result = [:] + + result = (data[JiraDataItem.TYPE_DOCS] ?: [:]) .values() - .findAll { issueIsWIP(it) } + .findAll { v -> issueIsWIPandMandatory(v, JiraDataItem.TYPE_DOCS) } .collect { chapter -> chapter.documents.collect { [doc: it, key: chapter.key] } }.flatten() @@ -459,13 +453,33 @@ class Project { .collectEntries { doc, issues -> [(doc as String): issues.collect { it.key } as List] } + + return result } @NonCPS - protected boolean issueIsWIP(Map issue) { - issue.status != null && - !issue.status.equalsIgnoreCase(JiraDataItem.ISSUE_STATUS_DONE) && - !issue.status.equalsIgnoreCase(JiraDataItem.ISSUE_STATUS_CANCELLED) + protected boolean isNonGxpManadatoryDoc(Map doc) { + return (doc.documents != null + && doc.number != null + && ((doc.documents.contains('CSD') && doc.number in ['1', '3.1']) || + (doc.documents.contains('SSDS') && doc.number in ['1', '2.1', '3.1', '5.4']))) + } + + @NonCPS + protected boolean issueIsWIPandMandatory(Map issue, String type) { + if (this.isGxpProject() || type != JiraDataItem.TYPE_DOCS) { + return (issue.status != null && + !issue.status.equalsIgnoreCase(JiraDataItem.ISSUE_STATUS_DONE) && + !issue.status.equalsIgnoreCase(JiraDataItem.ISSUE_STATUS_CANCELLED)) + } else { + return (isNonGxpManadatoryDoc(issue) && (issue.status != null && + !issue.status.equalsIgnoreCase(JiraDataItem.ISSUE_STATUS_DONE))) + } + } + + @NonCPS + boolean replaceIssueContentWithNonMandatoryText(Map issue) { + return !isGxpProject() && !issueIsWIPandMandatory(issue, JiraDataItem.TYPE_DOCS) } @NonCPS @@ -524,6 +538,7 @@ class Project { return this.data.jira.project.enumDictionary[name] } + @NonCPS Map getProjectProperties() { return this.data.jira.project.projectProperties } @@ -589,6 +604,12 @@ class Project { "${MROPipelineUtil.ODS_STATE_DIR}/${targetEnvironment}.json" } + @NonCPS + boolean isGxpProject() { + String isGxp = projectProperties?."PROJECT.IS_GXP" + return isGxp != null ? isGxp.toBoolean() : IS_GXP_PROJECT_DEFAULT + } + String getEnvStateFileName() { envStateFileName(buildParams.targetEnvironment) } @@ -1580,7 +1601,6 @@ class Project { new ProjectDataBitbucketRepository(steps).loadFile(savedVersion) } - /** * Saves the project data to the * @return filenames saved diff --git a/test/groovy/org/ods/orchestration/usecase/JiraUseCaseSpec.groovy b/test/groovy/org/ods/orchestration/usecase/JiraUseCaseSpec.groovy index 7ae0427dfe..20a29c4061 100644 --- a/test/groovy/org/ods/orchestration/usecase/JiraUseCaseSpec.groovy +++ b/test/groovy/org/ods/orchestration/usecase/JiraUseCaseSpec.groovy @@ -861,7 +861,9 @@ class JiraUseCaseSpec extends SpecHelper { ]) then: - 1 * jira.appendCommentToIssue("JIRA-4711", "${error.message}\n\nSee: ${steps.env.RUN_DISPLAY_URL}") + 1 * jira.appendCommentToIssue("JIRA-4711", + "${error.message}\n\nSee: ${steps.env.RUN_DISPLAY_URL}" + + "\n\nPlease note that for a successful Deploy to D, the above-mentioned issues need to be in status Done.") } def "update Jira release status result without error"() { diff --git a/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy b/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy index 9aa15f18b2..dd9cad88c6 100644 --- a/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy +++ b/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy @@ -17,9 +17,13 @@ import org.ods.orchestration.service.* import org.ods.orchestration.util.* import org.ods.util.IPipelineSteps import org.ods.util.Logger + +import javax.swing.text.Document import java.nio.file.Files import java.nio.file.NoSuchFileException +import static org.ods.orchestration.usecase.DocumentType.* +import static org.ods.orchestration.usecase.DocumentType.CSD import static util.FixtureHelper.* import util.* @@ -371,11 +375,11 @@ class LeVADocumentUseCaseSpec extends SpecHelper { where: documentType || template - DocumentType.CSD as String || (DocumentType.CSD as String) + "-999" - DocumentType.SSDS as String || (DocumentType.SSDS as String) + "-999" - DocumentType.CFTP as String || (DocumentType.CFTP as String) + "-999" - DocumentType.CFTR as String || (DocumentType.CFTR as String) + "-999" - DocumentType.RA as String || (DocumentType.RA as String) + CSD as String || (CSD as String) + "-999" + SSDS as String || (SSDS as String) + "-999" + CFTP as String || (CFTP as String) + "-999" + CFTR as String || (CFTR as String) + "-999" + RA as String || (RA as String) } def "create CSD"() { @@ -384,7 +388,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase = Spy(new LeVADocumentUseCase(project, steps, util, docGen, jenkins, jiraUseCase, junit, levaFiles, nexus, os, pdf, sq, bbt, logger)) // Argument Constraints - def documentType = DocumentType.CSD as String + def documentType = CSD as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE", key:"DEMO-1"]] @@ -460,7 +464,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { ] // Argument Constraints - def documentType = DocumentType.TRC as String + def documentType = TRC as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE", key:"DEMO-1"]] @@ -491,7 +495,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase = Spy(new LeVADocumentUseCase(project, steps, util, docGen, jenkins, jiraUseCase, junit, levaFiles, nexus, os, pdf, sq, bbt, logger)) // Argument Constraints - def documentType = DocumentType.DIL as String + def documentType = DIL as String def uri = "http://nexus" def documentTemplate = "template" def watermarkText = "WATERMARK" @@ -520,7 +524,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { def repo = project.repositories.first() // Argument Constraints - def documentType = DocumentType.DTP as String + def documentType = DTP as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -553,7 +557,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { def repo = project.repositories.first() // Argument Constraints - def documentType = DocumentType.DTP as String + def documentType = DTP as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -600,7 +604,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { ] // Argument Constraints - def documentType = DocumentType.DTR as String + def documentType = DTR as String def files = ["raw/${xmlFile.name}": xmlFile.bytes] // Stubbed Method Responses @@ -651,7 +655,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { ] // Argument Constraints - def documentType = DocumentType.DTR as String + def documentType = DTR as String def files = ["raw/${xmlFile.name}": xmlFile.bytes] // Stubbed Method Responses @@ -682,7 +686,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase = Spy(new LeVADocumentUseCase(project, steps, util, docGen, jenkins, jiraUseCase, junit, levaFiles, nexus, os, pdf, sq, bbt, logger)) // Argument Constraints - def documentType = DocumentType.CFTP as String + def documentType = CFTP as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -733,7 +737,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { ] // Argument Constraints - def documentType = DocumentType.CFTR as String + def documentType = CFTR as String def files = [ "raw/${xmlFile.name}": xmlFile.bytes ] // Stubbed Method Responses @@ -770,7 +774,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase = Spy(new LeVADocumentUseCase(project, steps, util, docGen, jenkins, jiraUseCase, junit, levaFiles, nexus, os, pdf, sq, bbt, logger)) // Argument Constraints - def documentType = DocumentType.TCP as String + def documentType = TCP as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -823,7 +827,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { ] // Argument Constraints - def documentType = DocumentType.TCR as String + def documentType = TCR as String def files = ["raw/${xmlFile.name}": xmlFile.bytes] // Stubbed Method Responses @@ -861,7 +865,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase = Spy(new LeVADocumentUseCase(project, steps, util, docGen, jenkins, jiraUseCase, junit, levaFiles, nexus, os, pdf, sq, bbt, logger)) // Argument Constraints - def documentType = DocumentType.IVP as String + def documentType = IVP as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -909,7 +913,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { ] // Argument Constraints - def documentType = DocumentType.IVR as String + def documentType = IVR as String def files = ["raw/${xmlFile.name}": xmlFile.bytes] // Stubbed Method Responses @@ -1006,7 +1010,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { FileUtils.copyDirectory(new FixtureHelper().getResource("Test-1.pdf").parentFile, tempFolder.getRoot()); def pdfDoc = new FixtureHelper().getResource("Test-1.pdf").bytes - def documentType = DocumentType.SSDS as String + def documentType = SSDS as String def uri = new URI("http://nexus") def pdfUtil = new PDFUtil() jiraUseCase = Spy(new JiraUseCase(project, steps, util, Mock(JiraService), logger)) @@ -1107,7 +1111,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase = Spy(new LeVADocumentUseCase(project, steps, util, docGen, jenkins, jiraUseCase, junit, levaFiles, nexus, os, pdf, sq, bbt, logger)) // Argument Constraints - def documentType = DocumentType.RA as String + def documentType = RA as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -1138,7 +1142,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase = Spy(new LeVADocumentUseCase(project, steps, util, docGen, jenkins, jiraUseCase, junit, levaFiles, nexus, os, pdf, sq, bbt, logger)) // Argument Constraints - def documentType = DocumentType.TIP as String + def documentType = TIP as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -1167,7 +1171,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { project.services.jira = null // Argument Constraints - def documentType = DocumentType.TIP as String + def documentType = TIP as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -1210,7 +1214,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { ] // Argument Constraints - def documentType = DocumentType.TIR as String + def documentType = TIR as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -1252,7 +1256,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { def repo = project.repositories.first() // Argument Constraints - def documentType = DocumentType.TIR as String + def documentType = TIR as String // Stubbed Method Responses def chapterData = ["sec1": [content: "myContent", status: "DONE"]] @@ -1277,8 +1281,8 @@ class LeVADocumentUseCaseSpec extends SpecHelper { def "create overall DTR"() { given: // Argument Constraints - def documentType = DocumentType.DTR as String - def documentTypeName = DocumentType.OVERALL_DTR as String + def documentType = DTR as String + def documentTypeName = OVERALL_DTR as String // Stubbed Method Responses def uri = "http://nexus" @@ -1296,8 +1300,8 @@ class LeVADocumentUseCaseSpec extends SpecHelper { def "create overall TIR"() { given: // Argument Constraints - def documentType = DocumentType.TIR as String - def documentTypeName = DocumentType.OVERALL_TIR as String + def documentType = TIR as String + def documentTypeName = OVERALL_TIR as String // Stubbed Method Responses def uri = "http://nexus" @@ -1916,22 +1920,40 @@ class LeVADocumentUseCaseSpec extends SpecHelper { [name: 'NAME',description: 'DESCRIPTION'] | 'DESCRIPTION' } - def "verify isGxpProject property"() { + def "replace content for non-mandatory open issues"() { given: - LeVADocumentUseCase leVADocumentUseCase = new LeVADocumentUseCase(null, null, null, - null, null, null, null, null, null, null, null, - null, null, null) + jiraUseCase = Spy(new JiraUseCase(project, steps, util, Mock(JiraService), logger)) + usecase = Spy(new LeVADocumentUseCase(project, steps, util, docGen, jenkins, jiraUseCase, junit, levaFiles, nexus, os, pdf, sq, bbt, logger)) + project.projectProperties."PROJECT.IS_GXP" = isGxp + project.data.jira.docs.doc2 = [ + documents: documentType, + status: status, + number: number, + section: "sec1", + content: "Original content"] when: - def result = leVADocumentUseCase.isGxpProject(projectProperties) + def result = usecase.getDocumentSections(documentType) then: - result == expected + result["sec1"].content == expected where: - projectProperties | expected - [:] | LeVADocumentUseCase.IS_GXP_PROJECT_DEFAULT - ['PROJECT.IS_GXP': 'false'] | false - ['PROJECT.IS_GXP': 'true'] | true + isGxp | documentType | status | number || expected + false | CSD as String | "IN PROGRESS" | "2" || "

Not mandatory.

" + false | SSDS as String | "IN PROGRESS" | "2" || "

Not mandatory.

" + false | CSD as String | "DONE" | "2" || "

Not mandatory.

" + false | SSDS as String | "DONE" | "2" || "

Not mandatory.

" + false | CSD as String | "DONE" | "1" || "

Not mandatory.

" + false | CSD as String | "CANCELLED" | "2" || "

Not mandatory.

" + false | SSDS as String | "CANCELLED" | "2" || "

Not mandatory.

" + false | CSD as String | "IN PROGRESS" | "1" || "Original content" + false | CSD as String | "IN PROGRESS" | "3.1" || "Original content" + false | SSDS as String | "IN PROGRESS" | "1" || "Original content" + false | SSDS as String | "IN PROGRESS" | "2.1" || "Original content" + false | SSDS as String | "IN PROGRESS" | "3.1" || "Original content" + false | SSDS as String | "IN PROGRESS" | "5.4" || "Original content" + true | CSD as String | "IN PROGRESS" | "2" || "Original content" } + } diff --git a/test/groovy/org/ods/orchestration/util/ProjectSpec.groovy b/test/groovy/org/ods/orchestration/util/ProjectSpec.groovy index a35d09a96b..fd91eb9b70 100644 --- a/test/groovy/org/ods/orchestration/util/ProjectSpec.groovy +++ b/test/groovy/org/ods/orchestration/util/ProjectSpec.groovy @@ -622,6 +622,10 @@ class ProjectSpec extends SpecHelper { "${type}-3": [ status: "DONE", key: "${type}-3", + ], + "${type}-4": [ + status: "CANCELLED", + key: "${type}-4", ] ] } @@ -638,6 +642,104 @@ class ProjectSpec extends SpecHelper { result == expected } + def "compute wip jira issues for non Gxp project"() { + given: + def data = [:] + Project.JiraDataItem.TYPES_WITH_STATUS.each { type -> + data[type] = [ + "${type}-1": [ + status: "TODO", + key: "${type}-1", + ], + "${type}-2": [ + status: "DOING", + key: "${type}-2", + ], + "${type}-3": [ + status: "DONE", + key: "${type}-3", + ], + "${type}-4": [ + status: "CANCELLED", + key: "${type}-4", + ], + "${type}-5": [ + status: Project.JiraDataItem.ISSUE_STATUS_TODO, + key: "${type}-5", + number: '1', + heading: 'Introduction', + documents:['SSDS'] + ], + "${type}-6": [ + status: Project.JiraDataItem.ISSUE_STATUS_DONE, + key: "${type}-6", + number: '2.1', + heading: 'System Design Overview', + documents:['SSDS'] + ], + "${type}-7": [ + status: Project.JiraDataItem.ISSUE_STATUS_CANCELLED, + key: "${type}-7", + number: '3.1', + heading: 'System Design Profile', + documents:['SSDS'] + ], + "${type}-8": [ + status: Project.JiraDataItem.ISSUE_STATUS_TODO, + key: "${type}-8", + number: '5.4', + heading: 'Utilisation of Existing Infrastructure Services', + documents:['SSDS'] + ], + "${type}-9": [ + status: Project.JiraDataItem.ISSUE_STATUS_TODO, + key: "${type}-9", + number: '1', + heading: 'Introduction and Purpose', + documents:['SSDS'] + ], + "${type}-10": [ + status: Project.JiraDataItem.ISSUE_STATUS_TODO, + key: "${type}-10", + number: '3.1', + heading: 'Related Business / GxP Process', + documents:['CSD'] + ], + "${type}-11": [ + status: Project.JiraDataItem.ISSUE_STATUS_TODO, + key: "${type}-11", + number: '5.1', + heading: 'Definitions', + documents:['CSD'] + ], + "${type}-12": [ + status: Project.JiraDataItem.ISSUE_STATUS_TODO, + key: "${type}-12", + number: '5.2', + heading: 'Abbreviations', + documents:['CSD'] + ] + ] + } + project.projectProperties.put(Project.IS_GXP_PROJECT_PROPERTY, 'false') + def expected = [:] + Project.JiraDataItem.COMMON_TYPES_TO_BE_CLOSED.each { type -> + expected[type] = [ "${type}-1", "${type}-2", "${type}-5", + "${type}-8", "${type}-9", "${type}-10", "${type}-11", "${type}-12", ] + } + expected[Project.JiraDataItem.TYPE_DOCS] = [ "${Project.JiraDataItem.TYPE_DOCS}-5", + "${Project.JiraDataItem.TYPE_DOCS}-7", + "${Project.JiraDataItem.TYPE_DOCS}-8", + "${Project.JiraDataItem.TYPE_DOCS}-9", + "${Project.JiraDataItem.TYPE_DOCS}-10"] + + when: + def result = project.computeWipJiraIssues(data) + + then: + result == expected + } + def "get wip Jira issues for an empty collection"() { setup: def data = [project: [:], components: [:]] @@ -899,6 +1001,132 @@ class ProjectSpec extends SpecHelper { } } + def "fail build with mandatory doc open issues for non-GxP project"() { + setup: + def data = [project: [:], components: [:]] + Project.JiraDataItem.TYPES_WITH_STATUS.each { type -> + data[type] = [ + "${type}-3": [ + status: "DONE" + ] + ] + } + + data.project.projectProperties = [:] + data.project.projectProperties["PROJECT.IS_GXP"] = "false" + data.docs = [:] + data.docs["docs-1"] = [ documents: [a], number: b, status: "DOING"] + def expected = [:] + Project.JiraDataItem.COMMON_TYPES_TO_BE_CLOSED.each { type -> + expected[type] = ["${type}-1", "${type}-2"] + } + + def expectedMessage = "The pipeline failed since the following issues are work in progress (no documents were generated): " + + expectedMessage += "\n\nDocs: docs-1" + project = createProject([ + "loadJiraData" : { + return data + }, + "loadJiraDataBugs": { + return [ + "bugs-3": [ + status: "DONE" + ] + ] + } + ]).init() + project.data.buildParams.version = "1.0" + when: + project.load(git, jiraUseCase) + + then: + project.hasWipJiraIssues() == c + + then: + def e = thrown(OpenIssuesException) + e.message == expectedMessage + + and: + Project.JiraDataItem.TYPES_WITH_STATUS.each { type -> + !expectedMessage.find("${type}-3") + } + + where: + a | b || c + "CSD" | '1' || true + "CSD" | '3.1' || true + "SSDS" | '1' || true + "SSDS" | '2.1' || true + "SSDS" | '3.1' || true + "SSDS" | '5.4' || true + } + + def "NOT fail build with non-mandatory doc open issues for non-GxP project"() { + setup: + def data = [project: [:], components: [:]] + Project.JiraDataItem.TYPES_WITH_STATUS.each { type -> + data[type] = [ + "${type}-3": [ + status: "DONE" + ] + ] + } + + data.project.projectProperties = [:] + data.project.projectProperties["PROJECT.IS_GXP"] = "false" + data.docs = [:] + data.docs["docs-1"] = [ documents: [a], number: b, status: "DOING"] + def expected = [:] + Project.JiraDataItem.COMMON_TYPES_TO_BE_CLOSED.each { type -> + expected[type] = ["${type}-1", "${type}-2"] + } + + def expectedMessage = "The pipeline failed since the following issues are work in progress (no documents were generated): " + + expectedMessage += "\n\nDocs: docs-1" + project = createProject([ + "loadJiraData" : { + return data + }, + "loadJiraDataBugs": { + return [ + "bugs-3": [ + status: "DONE" + ] + ] + } + ]).init() + project.data.buildParams.version = "1.0" + when: + project.load(git, jiraUseCase) + + then: + project.hasWipJiraIssues() == c + + where: + a | b || c + "CSD" | '2' || false + "CSD" | '3.2' || false + "SSDS" | '2' || false + "SSDS" | '2.2' || false + "SSDS" | '3.2' || false + "SSDS" | '5.5' || false + "CFTP" | '1' || false + "CFTR" | '1' || false + "DTP" | '1' || false + "DTR" | '1' || false + "DIL" | '1' || false + "IVP" | '1' || false + "IVR" | '1' || false + "RA" | '1' || false + "TCP" | '1' || false + "TCR" | '1' || false + "TIP" | '1' || false + "TIR" | '1' || false + "TRC" | '1' || false + } + def "load initial version"() { given: def component1 = [key: "CMP-1", name: "Component 1"] @@ -2703,6 +2931,110 @@ class ProjectSpec extends SpecHelper { result == expected } + def "compute WIP document chapters per document for non Gxp docs - all mandatory issues done"() { + given: + def issue = { String key, String number, String heading, String status, + List docs -> [(key): [ documents: docs, status: status, key: key, number: number, heading: heading ]] + } + + def data = [ + (Project.JiraDataItem.TYPE_DOCS): issue('77', '1', 'Introduction', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['SSDS']) + + issue('76', '2.1', 'System Design Overview', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['SSDS']) + + issue('73', '3.1', 'System Design Profile', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['SSDS']) + + issue('67', '5.4', 'Utilisation of Existing Infrastructure Services', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['SSDS']) + + issue('66', '6.1', 'Development Environment', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['SSDS']) + + issue('42', '1', 'Introduction and Purpose', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['CSD']) + + issue('40', '3.1', 'Related Business / GxP Process', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['CSD']) + + issue('39', '5.1', 'Definitions', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['CSD']) + + issue('38', '5.2', 'Abbreviations', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['CSD']) + ] + project.projectProperties.put(Project.IS_GXP_PROJECT_PROPERTY, 'false') + def expected = [:] + + when: + def result = project.computeWipDocChapterPerDocument(data) + + then: + result == expected + } + + def "compute WIP document chapters per document for non Gxp docs - one mandatory issue to do and one cancelled"() { + given: + def issue = { String key, String number, String heading, String status, + List docs -> [(key): [ documents: docs, status: status, key: key, number: number, heading: heading ]] + } + + def data = [ + (Project.JiraDataItem.TYPE_DOCS): issue('77', '1', 'Introduction', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['SSDS']) + + issue('76', '2.1', 'System Design Overview', + Project.JiraDataItem.ISSUE_STATUS_CANCELLED, ['SSDS']) + + issue('73', '3.1', 'System Design Profile', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['SSDS']) + + issue('67', '5.4', 'Utilisation of Existing Infrastructure Services', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['SSDS']) + + issue('42', '1', 'Introduction and Purpose', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['CSD']) + + issue('40', '3.1', 'Related Business / GxP Process', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['CSD']) + + issue('39', '5.1', 'Definitions', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['CSD']) + + issue('38', '5.2', 'Abbreviations', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['CSD']) + ] + project.projectProperties.put(Project.IS_GXP_PROJECT_PROPERTY, 'false') + def expected = [SSDS:['77', '76']] + + when: + def result = project.computeWipDocChapterPerDocument(data) + + then: + result == expected + } + + def "compute WIP document chapters per document for non Gxp docs - all issues to do"() { + given: + def issue = { String key, String number, String heading, String status, + List docs -> [(key): [ documents: docs, status: status, key: key, number: number, heading: heading ]] + } + + def data = [ + (Project.JiraDataItem.TYPE_DOCS): issue('77', '1', 'Introduction', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['SSDS']) + + issue('76', '2.1', 'System Design Overview', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['SSDS']) + + issue('73', '3.1', 'System Design Profile', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['SSDS']) + + issue('67', '5.4', 'Utilisation of Existing Infrastructure Services', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['SSDS']) + + issue('42', '1', 'Introduction and Purpose', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['CSD']) + + issue('40', '3.1', 'Related Business / GxP Process', + Project.JiraDataItem.ISSUE_STATUS_TODO, ['CSD']) + + issue('39', '5.1', 'Definitions', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['CSD']) + + issue('38', '5.2', 'Abbreviations', + Project.JiraDataItem.ISSUE_STATUS_DONE, ['CSD']) + ] + project.projectProperties.put(Project.IS_GXP_PROJECT_PROPERTY, 'false') + def expected = [SSDS:['77', '76', '73', '67'], CSD:['42', '40']] + + when: + def result = project.computeWipDocChapterPerDocument(data) + + then: + result == expected + } + def "assert if versioning is enabled for the project"() { given: def versionEnabled @@ -2853,4 +3185,36 @@ class ProjectSpec extends SpecHelper { "FALSE" || false } + def "verify isGxpProject default value"() { + given: + project.projectProperties.remove(Project.IS_GXP_PROJECT_PROPERTY) + + when: + def result = project.isGxpProject() + + then: + result == Project.IS_GXP_PROJECT_DEFAULT + } + + def "verify isGxpProject true value"() { + given: + project.projectProperties.put(Project.IS_GXP_PROJECT_PROPERTY, 'true') + + when: + def result = project.isGxpProject() + + then: + result == true + } + + def "verify isGxpProject false value"() { + given: + project.projectProperties.put(Project.IS_GXP_PROJECT_PROPERTY, 'false') + + when: + def result = project.isGxpProject() + + then: + result == false + } } diff --git a/test/resources/project-jira-data.json b/test/resources/project-jira-data.json index 874056ffc6..6f6dd0d7cb 100644 --- a/test/resources/project-jira-data.json +++ b/test/resources/project-jira-data.json @@ -26,7 +26,8 @@ "PROJECT.POO_CAT.HIGH": "Frequency of the usage of the related function is >10 times per week.", "PROJECT.POO_CAT.LOW": "Frequency of the usage of the related function is <10 times per year.", "PROJECT.POO_CAT.MEDIUM": "Frequency of the usage of the related function is <10 times per week.", - "PROJECT.USES_POO": "true" + "PROJECT.USES_POO": "true", + "PROJECT.IS_GXP": "true" }, "enumDictionary": { "ProbabilityOfDetection": {