diff --git a/vtl-prov/docs/model-v1.md b/vtl-prov/docs/model-v1.md index 518a2ee64..84cb9097b 100644 --- a/vtl-prov/docs/model-v1.md +++ b/vtl-prov/docs/model-v1.md @@ -13,6 +13,8 @@ his document present how we could extract some provenance from a VTL program in Based on `PROV-O` and `SDTH` ontologies. +_TODO: add kind of `cdi:RepresentedVariable`_ + ```mermaid classDiagram class Agent { diff --git a/vtl-prov/src/main/java/fr/insee/vtl/prov/ProvenanceListener.java b/vtl-prov/src/main/java/fr/insee/vtl/prov/ProvenanceListener.java index da0f9922b..c44fdc8e5 100644 --- a/vtl-prov/src/main/java/fr/insee/vtl/prov/ProvenanceListener.java +++ b/vtl-prov/src/main/java/fr/insee/vtl/prov/ProvenanceListener.java @@ -3,27 +3,29 @@ import fr.insee.vtl.parser.VtlBaseListener; import fr.insee.vtl.parser.VtlLexer; import fr.insee.vtl.parser.VtlParser; +import fr.insee.vtl.prov.prov.DataframeInstance; import fr.insee.vtl.prov.prov.Program; import fr.insee.vtl.prov.prov.ProgramStep; +import fr.insee.vtl.prov.prov.VariableInstance; import org.antlr.v4.runtime.*; import org.antlr.v4.runtime.misc.Interval; import org.antlr.v4.runtime.tree.ParseTreeWalker; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.Set; /** * ANTLR Listener that create provenance objects. */ public class ProvenanceListener extends VtlBaseListener { - private Program program = new Program(); - private Map programSteps = new HashMap<>(); + private final Program program = new Program(); private String currentProgramStep; + private boolean isInDatasetClause; + + private String currentComponentID; + public ProvenanceListener(String id, String programName) { program.setId(id); program.setLabel(programName); @@ -45,11 +47,10 @@ public void enterTemporaryAssignment(VtlParser.TemporaryAssignmentContext ctx) { String id = getText(ctx.varID()); String sourceCode = getText(ctx); currentProgramStep = id; - if (!programSteps.containsKey(id)) { - ProgramStep programStep = new ProgramStep(id, sourceCode); - programSteps.put(id, programStep); - } - program.getProgramStepIds().add(id); + ProgramStep programStep = new ProgramStep(id, id, sourceCode); + DataframeInstance df = new DataframeInstance(id, id); + programStep.setProducedDataframe(df); + program.getProgramSteps().add(programStep); } @Override @@ -62,11 +63,10 @@ public void enterPersistAssignment(VtlParser.PersistAssignmentContext ctx) { String id = getText(ctx.varID()); String sourceCode = getText(ctx); currentProgramStep = id; - if (!programSteps.containsKey(id)) { - ProgramStep programStep = new ProgramStep(id, sourceCode); - programSteps.put(id, programStep); - } - program.getProgramStepIds().add(id); + ProgramStep programStep = new ProgramStep(id, id, sourceCode); + DataframeInstance df = new DataframeInstance(id, id); + programStep.setProducedDataframe(df); + program.getProgramSteps().add(programStep); } @Override @@ -76,27 +76,76 @@ public void exitPersistAssignment(VtlParser.PersistAssignmentContext ctx) { @Override public void enterVarID(VtlParser.VarIDContext ctx) { + String id = ctx.IDENTIFIER().getText(); + if (!id.equals(currentProgramStep)) { + ProgramStep programStep = program.getProgramStepById(currentProgramStep); + if (!isInDatasetClause) { + Set consumedDataframe = programStep.getConsumedDataframe(); + DataframeInstance df = new DataframeInstance(id, id); + consumedDataframe.add(df); + } + if (isInDatasetClause && null != currentComponentID) { + Set usedVariables = programStep.getUsedVariables(); + VariableInstance v = new VariableInstance(id, id); + usedVariables.add(v); + } + } + } + @Override + public void enterDatasetClause(VtlParser.DatasetClauseContext ctx) { + isInDatasetClause = true; + } + + @Override + public void exitDatasetClause(VtlParser.DatasetClauseContext ctx) { + isInDatasetClause = false; + } + + @Override + public void enterComponentID(VtlParser.ComponentIDContext ctx) { + String id = ctx.getText(); + ProgramStep programStep = program.getProgramStepById(currentProgramStep); + Set assignedVariables = programStep.getAssignedVariables(); + VariableInstance v = new VariableInstance(id, id); + assignedVariables.add(v); + } + + @Override + public void enterCalcClauseItem(VtlParser.CalcClauseItemContext ctx) { + currentComponentID = getText(ctx.componentID()); + } + + @Override + public void exitCalcClauseItem(VtlParser.CalcClauseItemContext ctx) { + currentComponentID = null; + } + + @Override + public void enterAggrFunctionClause(VtlParser.AggrFunctionClauseContext ctx) { + currentComponentID = getText(ctx.componentID()); + } + + @Override + public void exitAggrFunctionClause(VtlParser.AggrFunctionClauseContext ctx) { + currentComponentID = null; } /** - * Returns the provenance objects + * Returns the program object */ - public List getObjects() { - List obj = new ArrayList<>(); - obj.add(program); - obj.addAll(programSteps.values()); - return obj; + public Program getProgram() { + return program; } - public static List parseAndListen(String expr, String id, String programName) { + public static Program run(String expr, String id, String programName) { CodePointCharStream stream = CharStreams.fromString(expr); VtlLexer lexer = new VtlLexer(stream); VtlParser parser = new VtlParser(new CommonTokenStream(lexer)); ProvenanceListener provenanceListener = new ProvenanceListener(id, programName); ParseTreeWalker.DEFAULT.walk(provenanceListener, parser.start()); - return provenanceListener.getObjects(); + return provenanceListener.getProgram(); } } diff --git a/vtl-prov/src/main/java/fr/insee/vtl/prov/RDFUtils.java b/vtl-prov/src/main/java/fr/insee/vtl/prov/RDFUtils.java index ee6ff5868..26fb91409 100644 --- a/vtl-prov/src/main/java/fr/insee/vtl/prov/RDFUtils.java +++ b/vtl-prov/src/main/java/fr/insee/vtl/prov/RDFUtils.java @@ -1,7 +1,9 @@ package fr.insee.vtl.prov; +import fr.insee.vtl.prov.prov.DataframeInstance; import fr.insee.vtl.prov.prov.Program; import fr.insee.vtl.prov.prov.ProgramStep; +import fr.insee.vtl.prov.prov.VariableInstance; import fr.insee.vtl.prov.utils.PROV; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; @@ -12,7 +14,10 @@ import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; -import java.util.List; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Set; public class RDFUtils { @@ -20,21 +25,15 @@ public class RDFUtils { private static final String TREVAS_BASE_URI = "http://trevas/"; private static final String SDTH_BASE_URI = "http://rdf-vocabulary.ddialliance.org/sdth#"; - public static Model buildModel(List objects) { + public static Model buildModel(Program program) { Model model = ModelFactory.createDefaultModel(); model.setNsPrefix("prov", PROV.getURI()); - objects.forEach(o -> { - if (o instanceof Program) { - handleProgram(model, (Program) o); - } - if (o instanceof ProgramStep) { - handleProgramStep(model, (ProgramStep) o); - } - }); + handleProgram(model, program); return model; } public static void handleProgram(Model model, Program program) { + // Create Program URI, type, label, sourceCode Resource SDTH_PROGRAM = model.createResource(SDTH_BASE_URI + "Program"); String id = program.getId(); String label = program.getLabel(); @@ -44,29 +43,75 @@ public static void handleProgram(Model model, Program program) { String sourceCode = program.getSourceCode(); Property SDTH_HAS_SOURCE_CODE = model.createProperty(SDTH_BASE_URI + "hasSourceCode"); programURI.addProperty(SDTH_HAS_SOURCE_CODE, sourceCode); - Set stepIds = program.getProgramStepIds(); - stepIds.forEach(stepId -> { - Property SDTH_HAS_PROGRAM_STEP = model.createProperty(SDTH_BASE_URI + "hasProgramStep"); - Resource SDTH_PROGRAM_STEP = model.createResource(SDTH_BASE_URI + "ProgramStep"); + // Link and define ProgramSteps + Set programSteps = program.getProgramSteps(); + Property SDTH_HAS_PROGRAM_STEP = model.createProperty(SDTH_BASE_URI + "hasProgramStep"); + programSteps.forEach(step -> { + String stepId = step.getId(); Resource programStepURI = model.createResource(TREVAS_BASE_URI + "program-step/" + stepId); programURI.addProperty(SDTH_HAS_PROGRAM_STEP, programStepURI); - programStepURI.addProperty(RDF.type, SDTH_PROGRAM_STEP); - programStepURI.addProperty(RDFS.label, "Create " + stepId + " dataset"); + handleProgramStep(model, step); }); } public static void handleProgramStep(Model model, ProgramStep programStep) { - String label = programStep.getLabel(); - Resource programStepURI = model.createResource(TREVAS_BASE_URI + "program-step/" + label); + // Create ProgramStep URI, type, label, sourceCode + String id = programStep.getId(); + Resource programStepURI = model.createResource(TREVAS_BASE_URI + "program-step/" + id); + Resource SDTH_PROGRAM_STEP = model.createResource(SDTH_BASE_URI + "ProgramStep"); + programStepURI.addProperty(RDF.type, SDTH_PROGRAM_STEP); + programStepURI.addProperty(RDFS.label, "Create " + id + " dataset"); String sourceCode = programStep.getSourceCode(); Property SDTH_HAS_SOURCE_CODE = model.createProperty(SDTH_BASE_URI + "hasSourceCode"); programStepURI.addProperty(SDTH_HAS_SOURCE_CODE, sourceCode); - Resource SDTH_DATAFRAME = model.createResource(SDTH_BASE_URI + "DataframeInstance"); - Resource dfProducesURI = model.createResource(TREVAS_BASE_URI + "dataset/" + label); - dfProducesURI.addProperty(RDF.type, SDTH_DATAFRAME); - dfProducesURI.addProperty(RDFS.label, label); + // Link and define producedDF + DataframeInstance dfProduced = programStep.getProducedDataframe(); + String dfProducedId = dfProduced.getId(); + Resource dfProducesURI = model.createResource(TREVAS_BASE_URI + "dataset/" + dfProducedId); Property SDTH_PRODUCES_DATAFRAME = model.createProperty(SDTH_BASE_URI + "producesDataframe"); programStepURI.addProperty(SDTH_PRODUCES_DATAFRAME, dfProducesURI); + handleDataframeInstance(model, dfProduced); + // Link and define consumedDF + Property SDTH_CONSUMES_DATAFRAME = model.createProperty(SDTH_BASE_URI + "consumesDataframe"); + programStep.getConsumedDataframe().forEach(df -> { + Resource dfConsumedURI = model.createResource(TREVAS_BASE_URI + "dataset/" + df.getId()); + programStepURI.addProperty(SDTH_CONSUMES_DATAFRAME, dfConsumedURI); + handleDataframeInstance(model, df); + }); + // Link and define usedVariables + Property SDTH_USED_VARIABLE = model.createProperty(SDTH_BASE_URI + "usesVariable"); + programStep.getUsedVariables().forEach(v -> { + Resource varUsedURI = model.createResource(TREVAS_BASE_URI + "variable/" + v.getId()); + programStepURI.addProperty(SDTH_USED_VARIABLE, varUsedURI); + handleVariableInstance(model, v); + }); + // Link and define assignedVariables + Property SDTH_ASSIGNED_VARIABLE = model.createProperty(SDTH_BASE_URI + "assignsVariable"); + programStep.getAssignedVariables().forEach(v -> { + Resource varAssignedURI = model.createResource(TREVAS_BASE_URI + "variable/" + v.getId()); + programStepURI.addProperty(SDTH_ASSIGNED_VARIABLE, varAssignedURI); + handleVariableInstance(model, v); + }); + } + + public static void handleDataframeInstance(Model model, DataframeInstance dfInstance) { + // Create DataframeInstance URI, type, label + String id = dfInstance.getId(); + Resource dfURI = model.createResource(TREVAS_BASE_URI + "dataset/" + id); + Resource SDTH_DATAFRAME = model.createResource(SDTH_BASE_URI + "DataframeInstance"); + dfURI.addProperty(RDF.type, SDTH_DATAFRAME); + String label = dfInstance.getLabel(); + dfURI.addProperty(RDFS.label, label); + } + + public static void handleVariableInstance(Model model, VariableInstance varInstance) { + // Create VariableInstance URI, type, label + String id = varInstance.getId(); + Resource varURI = model.createResource(TREVAS_BASE_URI + "variable/" + id); + Resource SDTH_VARIABLE = model.createResource(SDTH_BASE_URI + "VariableInstance"); + varURI.addProperty(RDF.type, SDTH_VARIABLE); + String label = varInstance.getLabel(); + varURI.addProperty(RDFS.label, label); } public static Model initModel(String baseFilePath) { @@ -87,4 +132,14 @@ public static void loadModelWithCredentials(Model model, connection.close(); } } + + public static void writeJsonLdToFile(Model model, String path) throws IOException { + model.write(Files.newOutputStream(Paths.get(path)), "JSON-LD"); + } + + public static String serialize(Model model, String format) { + StringWriter stringWriter = new StringWriter(); + model.write(stringWriter, format); + return stringWriter.toString(); + } } diff --git a/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/DataframeInstance.java b/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/DataframeInstance.java index f46776e26..2860dc6e4 100644 --- a/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/DataframeInstance.java +++ b/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/DataframeInstance.java @@ -1,4 +1,27 @@ package fr.insee.vtl.prov.prov; public class DataframeInstance { + String id; + String label; + + public DataframeInstance(String id, String label) { + this.id = id; + this.label = label; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } } diff --git a/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/Program.java b/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/Program.java index 813e14eca..ad11c7204 100644 --- a/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/Program.java +++ b/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/Program.java @@ -7,7 +7,7 @@ public class Program { String id; String label; - Set programStepIds = new HashSet<>(); + Set programSteps = new HashSet<>(); String sourceCode; @@ -35,12 +35,12 @@ public void setLabel(String label) { this.label = label; } - public Set getProgramStepIds() { - return programStepIds; + public Set getProgramSteps() { + return programSteps; } - public void setProgramSteps(Set programStepIds) { - this.programStepIds = programStepIds; + public void setProgramSteps(Set programSteps) { + this.programSteps = programSteps; } public String getSourceCode() { @@ -50,4 +50,11 @@ public String getSourceCode() { public void setSourceCode(String sourceCode) { this.sourceCode = sourceCode; } + + public ProgramStep getProgramStepById(String id) { + return programSteps.stream() + .filter(p -> p.getId().equals(id)) + .findFirst() + .orElse(null); + } } diff --git a/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/ProgramStep.java b/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/ProgramStep.java index 680e3a1ea..421999d72 100644 --- a/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/ProgramStep.java +++ b/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/ProgramStep.java @@ -1,18 +1,37 @@ package fr.insee.vtl.prov.prov; +import java.util.HashSet; +import java.util.Set; + public class ProgramStep { + String id; String label; String sourceCode; + Set usedVariables = new HashSet<>();; + Set assignedVariables = new HashSet<>();; + + Set consumedDataframe = new HashSet<>();; + DataframeInstance producedDataframe; + public ProgramStep() { } - public ProgramStep(String label, String sourceCode) { + public ProgramStep(String id, String label, String sourceCode) { + this.id = id; this.label = label; this.sourceCode = sourceCode; } + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + public String getLabel() { return label; } @@ -28,4 +47,36 @@ public String getSourceCode() { public void setSourceCode(String sourceCode) { this.sourceCode = sourceCode; } + + public Set getUsedVariables() { + return usedVariables; + } + + public void setUsedVariables(Set usedVariables) { + this.usedVariables = usedVariables; + } + + public Set getAssignedVariables() { + return assignedVariables; + } + + public void setAssignedVariables(Set assignedVariables) { + this.assignedVariables = assignedVariables; + } + + public Set getConsumedDataframe() { + return consumedDataframe; + } + + public void setConsumedDataframe(Set consumedDataframe) { + this.consumedDataframe = consumedDataframe; + } + + public DataframeInstance getProducedDataframe() { + return producedDataframe; + } + + public void setProducedDataframe(DataframeInstance producedDataframe) { + this.producedDataframe = producedDataframe; + } } diff --git a/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/VariableInstance.java b/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/VariableInstance.java index 629a00375..b7cc00707 100644 --- a/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/VariableInstance.java +++ b/vtl-prov/src/main/java/fr/insee/vtl/prov/prov/VariableInstance.java @@ -1,4 +1,27 @@ package fr.insee.vtl.prov.prov; public class VariableInstance { + String id; + String label; + + public VariableInstance(String id, String label) { + this.id = id; + this.label = label; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } } diff --git a/vtl-prov/src/test/java/fr/insee/vtl/prov/ProvenanceListenerTest.java b/vtl-prov/src/test/java/fr/insee/vtl/prov/ProvenanceListenerTest.java index d2e190d46..3f713c7a9 100644 --- a/vtl-prov/src/test/java/fr/insee/vtl/prov/ProvenanceListenerTest.java +++ b/vtl-prov/src/test/java/fr/insee/vtl/prov/ProvenanceListenerTest.java @@ -1,9 +1,8 @@ package fr.insee.vtl.prov; +import fr.insee.vtl.prov.prov.Program; import org.junit.jupiter.api.Test; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; public class ProvenanceListenerTest { @@ -14,7 +13,7 @@ public void simpleTest() { "ds_mul := ds_sum * 3; \n" + "ds_res <- ds_mul[filter mod(var1, 2) = 0][calc var_sum := var1 + var2];"; - List obj = ProvenanceListener.parseAndListen(script, "trevas-simple-test", "Simple test from Trevas tests"); - assertThat(obj).hasSize(4); + Program program = ProvenanceListener.run(script, "trevas-simple-test", "Simple test from Trevas tests"); + assertThat(program.getProgramSteps()).hasSize(4); } } \ No newline at end of file diff --git a/vtl-prov/src/test/java/fr/insee/vtl/prov/RDFTest.java b/vtl-prov/src/test/java/fr/insee/vtl/prov/RDFTest.java index cd3c0502e..36c53ba0d 100644 --- a/vtl-prov/src/test/java/fr/insee/vtl/prov/RDFTest.java +++ b/vtl-prov/src/test/java/fr/insee/vtl/prov/RDFTest.java @@ -1,12 +1,12 @@ package fr.insee.vtl.prov; +import fr.insee.vtl.prov.prov.Program; import fr.insee.vtl.prov.utils.PropertiesLoader; import org.apache.jena.rdf.model.Model; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.util.List; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; @@ -49,17 +49,63 @@ public void loadHandmadeRDF() { } @Test - public void simpleTest() { + public void simpleTest() throws IOException { String script = "ds_sum := ds1 + ds2;\n" + "ds_mul := ds_sum * 3; \n" + - "ds_res <- ds_mul[filter mod(var1, 2) = 0][calc var_sum := var1 + var2];"; + "ds_res <- ds_mul [filter mod(var1, 2) = 0]" + + " [calc var_sum := var1 + var2];"; - List obj = ProvenanceListener.parseAndListen(script, "trevas-simple-test", "Simple test from Trevas tests"); - Model model = RDFUtils.buildModel(obj); + Program program = ProvenanceListener.run(script, "trevas-simple-test", "Simple test from Trevas tests"); + Model model = RDFUtils.buildModel(program); + String content = RDFUtils.serialize(model, "JSON-LD"); + assertThat(content).isNotEmpty(); RDFUtils.loadModelWithCredentials(model, sparqlEndpoint, sparqlEndpointUser, sparlqEndpointPassword); - assertThat(obj).hasSize(4); + RDFUtils.writeJsonLdToFile(model, "src/test/resources/output/test-simple.json"); + assertThat(program.getProgramSteps()).hasSize(3); } + @Test + public void bpeTest() throws IOException { + + + String bpeScript = "// Validation of municipality code in input file\n" + + "CHECK_MUNICIPALITY := check_datapoint(BPE_DETAIL_VTL, UNIQUE_MUNICIPALITY invalid);\n" + + "\n" + + "// Clean BPE input database\n" + + "BPE_DETAIL_CLEAN := BPE_DETAIL_VTL [drop LAMBERT_X, LAMBERT_Y]\n" + + " [rename ID_EQUIPEMENT to id, TYPEQU to facility_type, DEPCOM to municipality, REF_YEAR to year];\n" + + "\n" + + "// BPE aggregation by municipality, type and year\n" + + "BPE_MUNICIPALITY <- BPE_DETAIL_CLEAN [aggr nb := count(id) group by municipality, year, facility_type];\n" + + "\n" + + "// BPE aggregation by NUTS 3, type and year\n" + + "BPE_NUTS3 <- BPE_MUNICIPALITY [calc nuts3 := if substr(municipality,1,2) = \"97\" then substr(municipality,1,3) else substr(municipality,1,2)]\n" + + " [aggr nb := count(nb) group by year, nuts3, facility_type];\n" + + "\n" + + "// BPE validation of facility types by NUTS 3\n" + + "CHECK_NUTS3_TYPES := check_datapoint(BPE_NUTS3, NUTS3_TYPES invalid);\n" + + "\n" + + "// Prepare 2021 census dataset by NUTS 3\n" + + "CENSUS_NUTS3_2021 := LEGAL_POP [rename REF_AREA to nuts3, TIME_PERIOD to year, POP_TOT to pop]\n" + + " [filter year = \"2021\"]\n" + + " [calc pop := cast(pop, integer)]\n" + + " [drop year, NB_COM, POP_MUNI];\n" + + "\n" + + "// Extract dataset on general practitioners from BPE by NUTS 3 in 2021\n" + + "GENERAL_PRACT_NUTS3_2021 := BPE_NUTS3 [filter facility_type = \"D201\" and year = \"2021\"]\n" + + " [drop facility_type, year];\n" + + "\n" + + "// Merge practitioners and legal population datasets by NUTS 3 in 2021 and compute an indicator\n" + + "BPE_CENSUS_NUTS3_2021 <- inner_join(GENERAL_PRACT_NUTS3_2021, CENSUS_NUTS3_2021)\n" + + " [calc pract_per_10000_inhabitants := nb / pop * 10000]\n" + + " [drop nb, pop];"; + + Program program = ProvenanceListener.run(bpeScript, "trevas-bpe-test", "BPE from Trevas tests"); + Model model = RDFUtils.buildModel(program); + RDFUtils.loadModelWithCredentials(model, sparqlEndpoint, sparqlEndpointUser, sparlqEndpointPassword); + RDFUtils.writeJsonLdToFile(model, "src/test/resources/output/test-bpe.json"); + assertThat(program.getProgramSteps()).hasSize(8); + } } diff --git a/vtl-prov/src/test/resources/.gitignore b/vtl-prov/src/test/resources/.gitignore new file mode 100644 index 000000000..7e902e850 --- /dev/null +++ b/vtl-prov/src/test/resources/.gitignore @@ -0,0 +1 @@ +output/* \ No newline at end of file