From 70f2db136b9e075d8faee3cdfa3111c02c6a9d96 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Fri, 22 Dec 2017 10:09:17 -0800 Subject: [PATCH] Validate that concourse job groups assignment is complete --- .../ide/vscode/concourse/ConcourseModel.java | 41 ++++++++++-- .../vscode/concourse/PipelineYmlSchema.java | 2 +- .../vscode/concourse/ConcourseEditorTest.java | 63 +++++++++++++++++++ 3 files changed, 99 insertions(+), 7 deletions(-) diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java index 3b8487470..a169c7283 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; @@ -32,30 +33,26 @@ import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; -import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider; import org.springframework.ide.vscode.commons.yaml.ast.YamlAstCache; import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; -import org.springframework.ide.vscode.commons.yaml.ast.YamlParser; import org.springframework.ide.vscode.commons.yaml.path.ASTRootCursor; import org.springframework.ide.vscode.commons.yaml.path.NodeCursor; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment; import org.springframework.ide.vscode.commons.yaml.path.YamlTraversal; import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; -import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache.NodeTypes; +import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.BasicYValueHint; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanUnionType; -import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; import org.springframework.ide.vscode.commons.yaml.schema.YValueHint; -import org.springframework.ide.vscode.commons.yaml.util.StaleFallbackCache; +import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; import org.springframework.ide.vscode.commons.yaml.util.Streams; -import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; @@ -131,6 +128,25 @@ public class ConcourseModel { } } + + + /** + * Verification of constraint: if at least one job is assigned to a group, then all jobs must be assigned to a group. + */ + public final void jobAssignmentIsComplete(DynamicSchemaContext dc, Node parent, Node node, YType type, IProblemCollector problems) { + Multiset assignedJobs = getStringsFromAst(dc.getDocument(), JOBS_ASSIGNED_TO_GROUPS); + if (assignedJobs!=null && !assignedJobs.isEmpty()) { + getJobNameNodes(dc).forEach(jobDefName -> { + String name = NodeUtil.asScalar(jobDefName); + if (StringUtil.hasText(name)) { //'not assigned to a group' errors for empty names are a bit silly, so avoid that + if (!assignedJobs.contains(name)) { + problems.accept(YamlSchemaProblems.schemaProblem("'"+name+"' belongs to no group", jobDefName)); + } + } + }); + } + } + /** * Get the job with given name. If there is no such job, or if there is more than one, this will return null. */ @@ -238,6 +254,14 @@ public class ConcourseModel { anyChild() ); + public static final YamlPath JOBS_ASSIGNED_TO_GROUPS = new YamlPath( + anyChild(), + valueAt("groups"), + anyChild(), + valueAt("jobs"), + anyChild() + ); + public static final YamlPath JOB_NAMES_PATH = new YamlPath( anyChild(), valueAt("jobs"), @@ -333,6 +357,11 @@ public class ConcourseModel { return getStringsFromAst(dc.getDocument(), JOB_NAMES_PATH); } + public Stream getJobNameNodes(DynamicSchemaContext dc) { + return getFromAst(dc.getDocument(), ast -> JOB_NAMES_PATH.traverseAmbiguously(ast)); + } + + private Multiset getStringsFromAst(IDocument doc, YamlPath path) { return getFromAst(doc, (ast) -> { return path diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index 728e5ffe2..6a705466e 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -399,7 +399,7 @@ public class PipelineYmlSchema implements YamlSchema { addProp(TOPLEVEL_TYPE, "resources", f.yseq(t_resource)); addProp(TOPLEVEL_TYPE, "jobs", f.yseq(job)); addProp(TOPLEVEL_TYPE, "resource_types", f.yseq(resourceType)); - addProp(TOPLEVEL_TYPE, "groups", f.yseq(group)); + addProp(TOPLEVEL_TYPE, "groups", f.yseq(group).require(models::jobAssignmentIsComplete)); definitionTypes = ImmutableList.of( jobNameDef, diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index 967fdc1a2..cd29cfb08 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -30,6 +30,7 @@ import org.springframework.ide.vscode.commons.util.IOUtil; import org.springframework.ide.vscode.commons.util.Unicodes; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions; +import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.languageserver.testharness.CodeAction; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; @@ -4313,6 +4314,68 @@ public class ConcourseEditorTest { editor.assertProblems("not-a-valid-version|Valid values are: [every, latest]"); } + @Test public void jobGroupsCompletenessReconcile() throws Exception { + //1) if all jobs are assigned to a group... don't complain: + Editor editor = harness.newEditor( + "jobs:\n" + + "- name: build-snapshot\n" + + "- name: build-release\n" + + "- name: test-snapshot\n" + + "- name: test-release\n" + + "- name: publish-snapshot\n" + + "- name: publish-release\n" + + "groups:\n" + + "- name: snapshot\n" + + " jobs:\n" + + " - build-snapshot\n" + + " - test-snapshot\n" + + " - publish-snapshot\n" + + "- name: release\n" + + " jobs:\n" + + " - build-release\n" + + " - test-release\n" + + " - publish-release\n" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems(/*NONE*/); + + //If there are no groups then, don't complain about inconmplete jobs partitioning + editor.setText( + "jobs:\n" + + "- name: build-snapshot\n" + + "- name: build-release\n" + + "- name: test-snapshot\n" + + "- name: test-release\n" + + "- name: publish-snapshot\n" + + "- name: publish-release\n" + ); + + //If at least one job is in a group, check that all jobs are in a group + editor.setText( + "jobs:\n" + + "- name: build-snapshot\n" + + "- name: build-release\n" + + "- name: test-snapshot\n" + + "- name: test-release\n" + + "- name: publish-snapshot\n" + + "- name: publish-release\n" + + "groups:\n" + + "- name: snapshot\n" + + " jobs:\n" + + " - build-snapshot\n" + + "- name: release\n" + + " jobs:\n" + + " - build-release" + ); + editor.assertProblems( + "test-snapshot|'test-snapshot' belongs to no group", + "test-release|'test-release' belongs to no group", + "publish-snapshot|'publish-snapshot' belongs to no group", + "publish-release|'publish-release' belongs to no group" + ); + + } + ////////////////////////////////////////////////////////////////////////////// private void assertContextualCompletions(String conText, String textBefore, String... textAfter) throws Exception {