├── .gitignore ├── screwdriver ├── pubring.gpg.enc ├── secring.gpg.enc ├── release.sh └── settings.xml ├── src ├── test │ ├── resources │ │ ├── pig-tests │ │ │ ├── data.txt │ │ │ └── sample.yaml │ │ ├── csv-tests │ │ │ ├── file.csv │ │ │ ├── sample.yaml │ │ │ └── test.yaml │ │ ├── log4j2.properties │ │ ├── sample-tests │ │ │ ├── empty-test.yaml │ │ │ ├── simple-tests.yaml │ │ │ └── tests.yaml │ │ ├── suppressions.xml │ │ ├── priority-tests │ │ │ └── tests.yaml │ │ ├── metadata-tests │ │ │ └── tests.yaml │ │ ├── ExpectedJUnitOutput.xml │ │ └── rest-tests │ │ │ └── sample.yaml │ └── java │ │ └── com │ │ └── yahoo │ │ └── validatar │ │ ├── FakeTestClass.java │ │ ├── common │ │ ├── QueryTest.java │ │ ├── HelpableTest.java │ │ ├── ExecutableTest.java │ │ ├── TestTest.java │ │ ├── PluggableTest.java │ │ └── ColumnTest.java │ │ ├── OutputCaptor.java │ │ ├── report │ │ ├── email │ │ │ ├── TestSuiteModelTest.java │ │ │ └── EmailFormatterTest.java │ │ ├── junit │ │ │ └── JUnitFormatterTest.java │ │ └── FormatManagerTest.java │ │ ├── parse │ │ ├── yaml │ │ │ └── YAMLTest.java │ │ └── ParseManagerTest.java │ │ ├── TestHelpers.java │ │ ├── AppTest.java │ │ └── execution │ │ └── fixed │ │ └── DSVTest.java └── main │ ├── resources │ ├── log4j2.properties │ └── checkstyle.xml │ ├── java │ └── com │ │ └── yahoo │ │ └── validatar │ │ ├── common │ │ ├── Metadata.java │ │ ├── Test.java │ │ ├── TestSuite.java │ │ ├── Helpable.java │ │ ├── TypedObject.java │ │ ├── Executable.java │ │ ├── Query.java │ │ ├── Pluggable.java │ │ ├── Column.java │ │ ├── Operations.java │ │ └── Operators.java │ │ ├── parse │ │ ├── Parser.java │ │ ├── yaml │ │ │ └── YAML.java │ │ ├── FileLoadable.java │ │ └── ParseManager.java │ │ ├── execution │ │ ├── Engine.java │ │ ├── fixed │ │ │ └── DSV.java │ │ ├── EngineManager.java │ │ ├── pig │ │ │ └── Sty.java │ │ └── hive │ │ │ └── Apiary.java │ │ ├── report │ │ ├── Formatter.java │ │ ├── email │ │ │ ├── TestSuiteModel.java │ │ │ └── EmailFormatter.java │ │ ├── junit │ │ │ └── JUnitFormatter.java │ │ └── FormatManager.java │ │ ├── assertion │ │ ├── Assertor.java │ │ └── Expression.java │ │ └── App.java │ └── antlr4 │ └── com │ └── yahoo │ └── validatar │ └── assertion │ └── Grammar.g4 ├── screwdriver.yaml ├── NOTICE ├── Makefile ├── Contributing.md └── Code-of-Conduct.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/** 2 | -------------------------------------------------------------------------------- /screwdriver/pubring.gpg.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/validatar/HEAD/screwdriver/pubring.gpg.enc -------------------------------------------------------------------------------- /screwdriver/secring.gpg.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/validatar/HEAD/screwdriver/secring.gpg.enc -------------------------------------------------------------------------------- /src/test/resources/pig-tests/data.txt: -------------------------------------------------------------------------------- 1 | foo,12 2 | bar,1 3 | foo,1 4 | foo,-4 5 | bar,0 6 | baz,-26 7 | foo,-8 8 | baz,19 9 | foo,2 10 | -------------------------------------------------------------------------------- /src/test/resources/csv-tests/file.csv: -------------------------------------------------------------------------------- 1 | fieldA,fieldB,fieldC,fieldD 2 | foo,15,34.2,bar 3 | baz,42,4.2,bar 4 | qux,0,8.4,norf 5 | foo,,,,, 6 | bar,,0.2, 7 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.properties: -------------------------------------------------------------------------------- 1 | status=warn 2 | 3 | appender.console.type=Console 4 | appender.console.name=STDOUT 5 | appender.console.layout.type=PatternLayout 6 | appender.console.layout.pattern=[%-5p] %d{ISO8601} %c - %m%n 7 | 8 | rootLogger.level=info 9 | rootLogger.appenderRef.stdout.ref=STDOUT 10 | -------------------------------------------------------------------------------- /src/test/resources/log4j2.properties: -------------------------------------------------------------------------------- 1 | status=warn 2 | 3 | appender.console.type=Console 4 | appender.console.name=STDOUT 5 | appender.console.layout.type=PatternLayout 6 | appender.console.layout.pattern=[%-5p] %d{ISO8601} %c - %m%n 7 | 8 | rootLogger.level=fatal 9 | rootLogger.appenderRef.stdout.ref=STDOUT 10 | -------------------------------------------------------------------------------- /src/test/resources/sample-tests/empty-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Do nothing 3 | description: Do nothing 4 | queries: 5 | - name: ALPHA 6 | engine: hive 7 | value: "SELECT 1 as ONE" 8 | tests: 9 | - name: Checking alpha 10 | description: Looks at count output. 11 | asserts: 12 | - ALPHA.ONE == 1 13 | ... 14 | -------------------------------------------------------------------------------- /src/test/resources/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/FakeTestClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | 9 | public class FakeTestClass implements Helpable { 10 | @Override 11 | public void printHelp() { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /screwdriver/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | export GPG_TTY=$(tty) 6 | 7 | openssl aes-256-cbc -pass pass:$GPG_ENCPHRASE -in screwdriver/pubring.gpg.enc -out screwdriver/pubring.gpg -d 8 | openssl aes-256-cbc -pass pass:$GPG_ENCPHRASE -in screwdriver/secring.gpg.enc -out screwdriver/secring.gpg -d 9 | 10 | mvn clean deploy --settings screwdriver/settings.xml -DskipTests=true -Possrh -Prelease 11 | 12 | rm -rf screwdriver/*.gpg 13 | -------------------------------------------------------------------------------- /screwdriver.yaml: -------------------------------------------------------------------------------- 1 | cache: 2 | pipeline: ["~/.m2"] 3 | 4 | shared: 5 | image: maven:3.6.3-jdk-8 6 | 7 | jobs: 8 | main: 9 | requires: [~pr, ~commit] 10 | secrets: 11 | - COVERALLS_TOKEN 12 | steps: 13 | - build: mvn -B clean verify 14 | - coverage: mvn coveralls:report 15 | 16 | release: 17 | requires: [~tag:/^validatar-\d+\.\d+\.\d+/] 18 | secrets: 19 | - SONATYPE_USERNAME 20 | - SONATYPE_PASSWORD 21 | - GPG_PASSPHRASE 22 | - GPG_ENCPHRASE 23 | steps: 24 | - publish: screwdriver/release.sh 25 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Metadata.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | 10 | /** 11 | * A wrapper type that represents a single piece of Metadata, i.e. a key value pair. 12 | */ 13 | @NoArgsConstructor @AllArgsConstructor 14 | public class Metadata { 15 | public String key; 16 | public String value; 17 | } 18 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2014, 2015 Yahoo! Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: full 2 | 3 | full: 4 | mvn clean javadoc:jar package 5 | 6 | clean: 7 | mvn clean 8 | 9 | test: 10 | mvn clean verify javadoc:javadoc 11 | 12 | jar: 13 | mvn clean package 14 | 15 | release: 16 | mvn -B release:prepare release:clean 17 | 18 | coverage: 19 | mvn clean clover2:setup test clover2:aggregate clover2:clover 20 | 21 | doc: 22 | mvn clean compile javadoc:javadoc 23 | 24 | see-coverage: coverage 25 | cd target/site/clover; python -m SimpleHTTPServer 26 | 27 | see-doc: doc 28 | cd target/site/apidocs; python -m SimpleHTTPServer 29 | 30 | fix-javadocs: 31 | mvn javadoc:fix -DfixClassComment=false -DfixFieldComment=false 32 | 33 | -------------------------------------------------------------------------------- /src/test/resources/priority-tests/tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Priority Example 3 | description: Simple queries with priority 4 | queries: 5 | - name: No priority 6 | engine: hive 7 | value: "" 8 | - name: Priority A 9 | engine: hive 10 | value: "" 11 | priority: -1 12 | - name: Priority B 13 | engine: hive 14 | value: "" 15 | priority: 0 16 | - name: Priority C 17 | engine: hive 18 | value: "" 19 | priority: 1 20 | - name: Priority D 21 | engine: hive 22 | value: "" 23 | priority: 2 24 | tests: 25 | - name: Testing something 26 | description: Why I am testing this. 27 | asserts: 28 | ... 29 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/common/QueryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import org.testng.Assert; 8 | import org.testng.annotations.Test; 9 | 10 | public class QueryTest { 11 | @Test 12 | public void testGetSet() { 13 | Query query = new Query(); 14 | 15 | query.setFailure("sample message"); 16 | Assert.assertEquals(query.getMessages().size(), 1); 17 | Assert.assertEquals(query.getMessages().get(0), "sample message"); 18 | 19 | Assert.assertTrue(query.failed()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Test.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import lombok.Getter; 8 | 9 | import java.util.List; 10 | 11 | public class Test extends Executable { 12 | public String name; 13 | public String description; 14 | public List asserts; 15 | @Getter 16 | public boolean warnOnly = false; 17 | 18 | /** 19 | * Did this test pass. A test passes if it only warns. 20 | * 21 | * @return A boolean denoting if the test passed or not. 22 | */ 23 | public boolean passed() { 24 | return warnOnly || !failed; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/parse/Parser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.parse; 6 | 7 | import com.yahoo.validatar.common.TestSuite; 8 | 9 | import java.io.InputStream; 10 | 11 | public interface Parser { 12 | /** 13 | * Parse the TestSuite from an InputStream. 14 | * 15 | * @param data The InputStream containing the tests. 16 | * @return A TestSuite object representing the parsed testfile. 17 | */ 18 | TestSuite parse(InputStream data); 19 | 20 | /** 21 | * Returns the name of this Parser. 22 | * 23 | * @return The {@link java.lang.String} name. 24 | */ 25 | String getName(); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/parse/yaml/YAML.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.parse.yaml; 6 | 7 | import com.yahoo.validatar.common.TestSuite; 8 | import com.yahoo.validatar.parse.Parser; 9 | import org.yaml.snakeyaml.Yaml; 10 | import org.yaml.snakeyaml.constructor.Constructor; 11 | 12 | import java.io.InputStream; 13 | 14 | public class YAML implements Parser { 15 | public static final String NAME = "yaml"; 16 | 17 | @Override 18 | public TestSuite parse(InputStream data) { 19 | Yaml yaml = new Yaml(new Constructor(TestSuite.class)); 20 | return (TestSuite) yaml.load(data); 21 | } 22 | 23 | @Override 24 | public String getName() { 25 | return NAME; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/parse/FileLoadable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.parse; 6 | 7 | import com.yahoo.validatar.common.TestSuite; 8 | 9 | import java.io.File; 10 | import java.io.FileNotFoundException; 11 | import java.util.List; 12 | 13 | public interface FileLoadable { 14 | /** 15 | * Load test file(s) from a provided path. If a folder, load all test files. If a file, load it. 16 | * 17 | * @param path The folder with the test file(s) or the test file.j 18 | * @return A non null list of TestSuites representing the TestSuites in path. Empty if no TestSuite found. 19 | * @throws java.io.FileNotFoundException if any. 20 | */ 21 | List load(File path) throws FileNotFoundException; 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/common/HelpableTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import com.yahoo.validatar.OutputCaptor; 8 | import joptsimple.OptionParser; 9 | import org.mockito.Mockito; 10 | import org.testng.annotations.Test; 11 | 12 | import java.io.IOException; 13 | import java.io.OutputStream; 14 | 15 | public class HelpableTest { 16 | @Test(expectedExceptions = {RuntimeException.class}) 17 | public void testFailPrintHelp() throws IOException { 18 | OptionParser mocked = Mockito.mock(OptionParser.class); 19 | Mockito.doThrow(new IOException()).when(mocked).printHelpOn(Mockito.any(OutputStream.class)); 20 | OutputCaptor.runWithoutOutput(() -> Helpable.printHelp("", mocked)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/sample-tests/simple-tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Simple examples 3 | description: Simple tests for Validatar. 4 | queries: 5 | - name: ALPHA 6 | engine: hive 7 | value: "SELECT COUNT(1) as count 8 | FROM hourly_data 9 | WHERE dt=${DATE}" 10 | - name: BETA 11 | engine: hive 12 | value: "SELECT SUM(CASE WHEN y = true THEN 1 ELSE 0 END) as y_count 13 | FROM sample_data TABLESAMPLE(0.1 percent) 14 | WHERE dt=${DATE}" 15 | - name: FAIL 16 | engine: pig 17 | value: "LOAD '${FILE}' USING PigStorage(','); 18 | BAD_TYPO_HERE" 19 | tests: 20 | - name: Checking alpha 21 | description: Looks at count output. 22 | asserts: 23 | - ALPHA.count > 10000 24 | - name: Checking beta 25 | description: Long description of the second query 26 | asserts: 27 | - BETA.y < 500 28 | ... 29 | -------------------------------------------------------------------------------- /src/test/resources/metadata-tests/tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Metadata Example 3 | description: Simple queries with metadata 4 | queries: 5 | - name: No metadata 6 | engine: hive 7 | value: "SELECT 1 AS constant 8 | FROM sample_data TABLESAMPLE(0.1 percent) 9 | WHERE dt=${DATE}" 10 | - name: Metadata 11 | engine: hive 12 | value: "SELECT SUM(timestamp) AS total 13 | FROM hourly_data TABLESAMPLE(0.1 percent) 14 | WHERE dt=${DATE}" 15 | metadata: 16 | - key: records 17 | value: 10000 18 | - key: windowSize 19 | value: 600 20 | - name: Different Metadata 21 | engine: hive 22 | value: "SELECT COUNT(1) AS total 23 | FROM hourly_data 24 | WHERE dt=${DATE}" 25 | metadata: 26 | - key: threads 27 | value: 4 28 | tests: 29 | - name: Testing something 30 | description: Why I am testing this. 31 | asserts: 32 | ... 33 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/TestSuite.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import java.util.List; 8 | 9 | public class TestSuite { 10 | public String name; 11 | public String description; 12 | public List queries; 13 | public List tests; 14 | 15 | /** 16 | * Checks to see if this had any failures. For this method, tests are marked as failed even if they are warn only. 17 | * 18 | * @return A boolean denoting whether there were any errors in this suite. 19 | */ 20 | public boolean hasFailures() { 21 | // Even if tests were set to warn only want to mark if 22 | return hasError(queries) || hasError(tests); 23 | } 24 | 25 | private static boolean hasError(List executables) { 26 | return executables != null && executables.stream().anyMatch(Executable::failed); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /screwdriver/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ossrh 5 | ${env.SONATYPE_USERNAME} 6 | ${env.SONATYPE_PASSWORD} 7 | 8 | 9 | 10 | 11 | 12 | ossrh 13 | 14 | true 15 | 16 | 17 | gpg 18 | ${env.GPG_PASSPHRASE} 19 | false 20 | ${env.SD_SOURCE_DIR}/screwdriver 21 | pubring.gpg 22 | secring.gpg 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/execution/Engine.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.execution; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | import com.yahoo.validatar.common.Query; 9 | 10 | public interface Engine extends Helpable { 11 | /** 12 | * Setups the engine using the input parameters. 13 | * 14 | * @param arguments An array of parameters of the form [--param1 value1 --param2 value2...] 15 | * @return true iff setup was succesful. 16 | */ 17 | boolean setup(String[] arguments); 18 | 19 | /** 20 | * Executes the given query on this Engine and places the result into it. 21 | * 22 | * @param query The query object representing the query. 23 | */ 24 | void execute(Query query); 25 | 26 | /** 27 | * Returns the name of the engine. Ex: 'Hive', 'Pig', etc. 28 | * 29 | * @return Name of the engine. 30 | */ 31 | String getName(); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Helpable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import joptsimple.OptionParser; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * Any class that needs to print help should implement this. 13 | */ 14 | public interface Helpable { 15 | /** 16 | * Prints help to System.out. 17 | */ 18 | void printHelp(); 19 | 20 | /** 21 | * Prints the parser's help with the given header. 22 | * @param header A String header to write. 23 | * @param parser A @link {joptsimple.OptionParser} parser that will be used to print help to System.out. 24 | */ 25 | static void printHelp(String header, OptionParser parser) { 26 | System.out.println("\n" + header + ":"); 27 | try { 28 | parser.printHelpOn(System.out); 29 | } catch (IOException e) { 30 | throw new RuntimeException(e); 31 | } 32 | System.out.println(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/resources/pig-tests/sample.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pig test 3 | description: Testing Validatar 4 | queries: 5 | - name: Query 6 | engine: pig 7 | value: "a = LOAD 'src/test/resources/pig-tests/data.txt' USING PigStorage(',') AS (name:chararray, count:long); 8 | b = GROUP a BY name; 9 | c = FOREACH b GENERATE 10 | group AS name, 11 | SUM(a.count) AS total; 12 | d = ORDER c BY total DESC; 13 | e = LIMIT d 1;" 14 | metadata: 15 | - key: output-alias 16 | value: e 17 | - key: exec-type 18 | value: local 19 | - name: Defaults 20 | engine: pig 21 | value: "a = LOAD 'src/test/resources/pig-tests/data.txt' USING PigStorage(',') AS (name:chararray, count:long); 22 | b = ORDER a BY count; 23 | validatar_results = LIMIT b 1;" 24 | tests: 25 | - name: Simple Test 26 | description: Why I am testing this. 27 | asserts: 28 | - Query.name == "foo" 29 | - Query.count == 3 30 | - Defaults.name == "baz" 31 | - Defaults.count == -26 32 | ... -------------------------------------------------------------------------------- /src/test/resources/ExpectedJUnitOutput.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/OutputCaptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar; 6 | 7 | import org.apache.commons.io.output.NullOutputStream; 8 | 9 | import java.io.FileDescriptor; 10 | import java.io.FileOutputStream; 11 | import java.io.PrintStream; 12 | 13 | public class OutputCaptor { 14 | public static final PrintStream NULL = new PrintStream(NullOutputStream.NULL_OUTPUT_STREAM); 15 | public static final PrintStream OUT = new PrintStream(new FileOutputStream(FileDescriptor.out)); 16 | public static final PrintStream ERR = new PrintStream(new FileOutputStream(FileDescriptor.err)); 17 | 18 | public static void redirectToDevNull() { 19 | System.setOut(NULL); 20 | System.setErr(NULL); 21 | } 22 | 23 | public static void redirectToStandard() { 24 | System.setOut(OUT); 25 | System.setErr(ERR); 26 | } 27 | 28 | public static void runWithoutOutput(Runnable function) { 29 | redirectToDevNull(); 30 | function.run(); 31 | redirectToStandard(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/resources/csv-tests/sample.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CSV Example 3 | description: CSV and other delimited data 4 | queries: 5 | - name: StringTest 6 | engine: csv 7 | value: | 8 | A,B,C 9 | foo,234.3,bar 10 | baz,9,qux 11 | foo,42,norf 12 | metadata: 13 | - key: B 14 | value: DOUBLE 15 | - name: FileLoadingTest 16 | engine: csv 17 | value: "src/test/resources/csv-tests/file.csv" 18 | metadata: 19 | - key: fieldB 20 | value: LONG 21 | - key: fieldC 22 | value: DECIMAL 23 | - name: CustomDelimASCII 24 | engine: csv 25 | value: | 26 | A;B;C 27 | foo;234.3;bar 28 | baz;9;qux 29 | foo;42;norf 30 | metadata: 31 | - key: delimiter 32 | value: ";" 33 | - name: CustomDelimUnicode 34 | engine: csv 35 | value: | 36 | A\u0001B\u0001C 37 | foo\u0001234.3\u0001bar 38 | baz\u00019\u0001qux 39 | foo\u000142\u0001norf 40 | metadata: 41 | - key: delimiter 42 | value: "\u0001" 43 | tests: 44 | - name: Testing something 45 | description: Why I am testing this. 46 | asserts: 47 | ... 48 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/report/Formatter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.report; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | import com.yahoo.validatar.common.TestSuite; 9 | 10 | import java.io.IOException; 11 | import java.util.List; 12 | 13 | /** 14 | * Interface for writing test report files. 15 | */ 16 | public interface Formatter extends Helpable { 17 | /** 18 | * Setups the engine using the input parameters. 19 | * 20 | * @param arguments An array of parameters of the form [--param1 value1 --param2 value2...] 21 | * @return true iff setup was successful. 22 | */ 23 | boolean setup(String[] arguments); 24 | 25 | /** 26 | * Write the results of the test suites out. 27 | * 28 | * @param testSuites List of TestSuites to generate the report with 29 | * @throws java.io.IOException if any. 30 | */ 31 | void writeReport(List testSuites) throws IOException; 32 | 33 | /** 34 | * Returns the name of the Formatter. Ex: 'JUnit' etc. 35 | * 36 | * @return Name of the formatter. 37 | */ 38 | String getName(); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/TypedObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * This is the custom annotated object that is used in our assertion language. 11 | */ 12 | public class TypedObject { 13 | /** 14 | * We are now handling type safety. 15 | */ 16 | @SuppressWarnings("unchecked") 17 | // TODO: Change these to final or use getters/setters. 18 | public Comparable data; 19 | public TypeSystem.Type type; 20 | 21 | /** 22 | * Constructor. 23 | * 24 | * @param data A non-null {@link java.lang.Comparable} object that we are managing the type for. 25 | * @param type The non-null {@link com.yahoo.validatar.common.TypeSystem.Type} of the object. 26 | */ 27 | public TypedObject(Comparable data, TypeSystem.Type type) { 28 | Objects.requireNonNull(data); 29 | Objects.requireNonNull(type); 30 | this.data = data; 31 | this.type = type; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "<" + data.toString() + ", " + type.toString() + ">"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/resources/sample-tests/tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Validatar Example 3 | description: Simple tests for some data 4 | queries: 5 | - name: PL 6 | engine: hive 7 | value: "SELECT SUM(CASE WHEN is_page_view = true THEN 1 ELSE 0 END) AS pv_count, 8 | SUM(CASE WHEN is_logged_in = true THEN 1 ELSE 0 END) as logged_in_count 9 | FROM hourly_data TABLESAMPLE(0.1 percent) 10 | WHERE dt=${DATE}" 11 | - name: XY 12 | engine: hive 13 | value: "SELECT SUM(CASE WHEN x = true THEN 1 ELSE 0 END) AS x_count, 14 | SUM(CASE WHEN y = true THEN 1 ELSE 0 END) as y_count 15 | FROM sample_data TABLESAMPLE(0.1 percent) 16 | WHERE dt=${DATE}" 17 | - name: TEST 18 | engine: Hive 19 | value: "TEST ${DATE}" 20 | tests: 21 | - name: Pageview and logged in 22 | description: Pageview and logged in events are present, and correct relationships. 23 | warnOnly: true 24 | asserts: 25 | - PL.pv_count > 10000 26 | - PL.logged_in_count < PL>pv_count 27 | - PL.pv_count !=0 && PL.logged_in_count != 0 28 | - name: Second query 29 | description: Long description of the second query 30 | asserts: 31 | - XY.x > 100 32 | - XY.y < 500 33 | - XY.y < XY.x 34 | - name: Third query 35 | description: Long description of the third query 36 | asserts: 37 | ... 38 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | First, thanks for taking the time to contribute to our project! The following information provides a guide for making contributions. 3 | 4 | ## Code of Conduct 5 | 6 | By participating in this project, you agree to abide by the [Yahoo Code of Conduct](Code-of-Conduct.md). Everyone is welcome to submit a pull request or open an issue to improve the documentation, add improvements, or report bugs. 7 | 8 | ## How to Ask a Question 9 | 10 | If you simply have a question that needs an answer, [create an issue](https://help.github.com/articles/creating-an-issue/), and label it as a question. 11 | 12 | ## How To Contribute 13 | 14 | ### Report a Bug or Request a Feature 15 | 16 | If you encounter any bugs while using this software, or want to request a new feature or enhancement, feel free to [create an issue](https://help.github.com/articles/creating-an-issue/) to report it, make sure you add a label to indicate what type of issue it is. 17 | 18 | ### Contribute Code 19 | Pull requests are welcome for bug fixes. If you want to implement something new, please [request a feature first](#report-a-bug-or-request-a-feature) so we can discuss it. 20 | 21 | #### Creating a Pull Request 22 | Please follow [best practices](https://github.com/trein/dev-best-practices/wiki/Git-Commit-Best-Practices) for creating git commits. 23 | 24 | When your code is ready to be submitted, you can [submit a pull request](https://help.github.com/articles/creating-a-pull-request/) to begin the code review process. 25 | 26 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/common/ExecutableTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import org.testng.Assert; 8 | import org.testng.annotations.Test; 9 | 10 | public class ExecutableTest { 11 | private class Annotated extends Executable { 12 | } 13 | 14 | @Test 15 | public void testInitials() { 16 | Annotated sample = new Annotated(); 17 | 18 | Assert.assertFalse(sample.failed()); 19 | Assert.assertNull(sample.getMessages()); 20 | } 21 | 22 | @Test 23 | public void testRanState() { 24 | Annotated sample = new Annotated(); 25 | 26 | Assert.assertFalse(sample.failed()); 27 | sample.setFailed(); 28 | Assert.assertTrue(sample.failed()); 29 | sample.setSuccess(); 30 | Assert.assertFalse(sample.failed()); 31 | } 32 | 33 | @Test 34 | public void testMessages() { 35 | Annotated sample = new Annotated(); 36 | 37 | sample.addMessage("Test 1"); 38 | Assert.assertEquals(sample.getMessages().size(), 1); 39 | Assert.assertEquals(sample.getMessages().get(0), "Test 1"); 40 | 41 | sample.addMessage("Test 2"); 42 | Assert.assertEquals(sample.getMessages().size(), 2); 43 | Assert.assertEquals(sample.getMessages().get(0), "Test 1"); 44 | Assert.assertEquals(sample.getMessages().get(1), "Test 2"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/report/email/TestSuiteModelTest.java: -------------------------------------------------------------------------------- 1 | package com.yahoo.validatar.report.email; 2 | 3 | import com.yahoo.validatar.common.Query; 4 | import com.yahoo.validatar.common.TestSuite; 5 | import org.testng.annotations.Test; 6 | 7 | import java.util.Arrays; 8 | 9 | import static junit.framework.Assert.assertFalse; 10 | import static junit.framework.Assert.assertTrue; 11 | import static org.junit.Assert.assertEquals; 12 | 13 | public class TestSuiteModelTest { 14 | @Test 15 | public void testConstructorCountsPassedQueriesAndTests() { 16 | TestSuite ts = new TestSuite(); 17 | com.yahoo.validatar.common.Test passedTest = new com.yahoo.validatar.common.Test(); 18 | assertTrue(passedTest.passed()); 19 | com.yahoo.validatar.common.Test failedTest = new com.yahoo.validatar.common.Test(); 20 | failedTest.setFailed(); 21 | assertFalse(failedTest.passed()); 22 | Query passedQuery = new Query(); 23 | assertFalse(passedQuery.failed()); 24 | Query failedQuery = new Query(); 25 | failedQuery.setFailed(); 26 | assertTrue(failedQuery.failed()); 27 | ts.queries = Arrays.asList(failedQuery, passedQuery, failedQuery, passedQuery, passedQuery); 28 | ts.tests = Arrays.asList(passedTest, failedTest, passedTest, failedTest, failedTest); 29 | TestSuiteModel model = new TestSuiteModel(ts); 30 | assertEquals(5, model.queryTotal); 31 | assertEquals(5, model.testTotal); 32 | assertEquals(3, model.queryPassed); 33 | assertEquals(2, model.testPassed); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/common/TestTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import org.testng.Assert; 8 | import org.testng.annotations.Test; 9 | import java.io.FileNotFoundException; 10 | 11 | import static com.yahoo.validatar.TestHelpers.getTestSuiteFrom; 12 | 13 | public class TestTest { 14 | @Test 15 | public void testGetSet() { 16 | com.yahoo.validatar.common.Test test = new com.yahoo.validatar.common.Test(); 17 | 18 | test.addMessage("sample message"); 19 | Assert.assertEquals(test.getMessages().size(), 1); 20 | Assert.assertEquals(test.getMessages().get(0), "sample message"); 21 | 22 | test.setFailed(); 23 | Assert.assertTrue(test.failed()); 24 | Assert.assertFalse(test.passed()); 25 | 26 | test.setSuccess(); 27 | Assert.assertFalse(test.failed()); 28 | Assert.assertTrue(test.passed()); 29 | 30 | test.warnOnly = true; 31 | Assert.assertTrue(test.passed()); 32 | test.setFailed(); 33 | Assert.assertTrue(test.passed()); 34 | } 35 | 36 | @Test 37 | public void testLoadingWithWarnOnly() throws FileNotFoundException { 38 | TestSuite suite = getTestSuiteFrom("sample-tests/tests.yaml"); 39 | 40 | com.yahoo.validatar.common.Test test = suite.tests.get(0); 41 | Assert.assertTrue(test.warnOnly); 42 | 43 | test = suite.tests.get(1); 44 | Assert.assertFalse(test.warnOnly); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Executable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * This is an abstraction of something that can be "run" to produce something. The 12 | * running of it can cause failure and generate messages. This abstracts that out. 13 | */ 14 | public abstract class Executable { 15 | protected boolean failed = false; 16 | protected List messages = null; 17 | 18 | /** 19 | * Get messages. 20 | * 21 | * @return A {@link java.util.List} of messages. 22 | */ 23 | public List getMessages() { 24 | return messages; 25 | } 26 | 27 | /** 28 | * Add a message. 29 | * 30 | * @param message The message to add. 31 | */ 32 | public void addMessage(String message) { 33 | if (this.messages == null) { 34 | this.messages = new ArrayList<>(); 35 | } 36 | this.messages.add(message); 37 | } 38 | 39 | /** 40 | * Set failure status. 41 | */ 42 | public void setFailed() { 43 | this.failed = true; 44 | } 45 | 46 | /** 47 | * Set success status. 48 | */ 49 | public void setSuccess() { 50 | this.failed = false; 51 | } 52 | 53 | /** 54 | * Is the executable in the failed status? 55 | * 56 | * @return True iff in the failure status. 57 | */ 58 | public boolean failed() { 59 | return failed; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/resources/csv-tests/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Click and View Thresholds 3 | description: Test a bunch of click and view metrics for regions with joins 4 | queries: 5 | - name: A 6 | engine: csv 7 | value: | 8 | date,country,views,clicks 9 | 20170101,us,10000,124 10 | 20170101,uk,4340,14 11 | 20170101,fr,4520,0 12 | 20170101,cn,99999,1024 13 | 20170101,eg,100,24 14 | 20170102,us,9900,328 15 | 20170102,uk,2340,13 16 | 20170102,fr,4313,20 17 | 20170102,cn,97345,2034 18 | 20170102,eg,100,24 19 | 20170102,sa,0,2 20 | metadata: 21 | - key: views 22 | value: LONG 23 | - key: clicks 24 | value: LONG 25 | - name: B 26 | engine: csv 27 | value: | 28 | country,continent,threshold,expected 29 | us,na,0.01,10090 30 | uk,eu,0.1,4100 31 | fr,eu,0.0,4500 32 | cn,as,0.05,100000 33 | eg,af,0.15,110 34 | sa,af,0.1,10 35 | au,au,0.2,5 36 | metadata: 37 | - key: threshold 38 | value: DOUBLE 39 | - key: expected 40 | value: LONG 41 | tests: 42 | - name: All clicks and views should be non-negative 43 | description: Basic integrity check 44 | asserts: 45 | - A.clicks >= 0 && A.views >= 0 46 | - name: Views are within threshold for non-Asian countries 47 | description: The views for each day is within the margin of error threshold for all countries not in Asia 48 | asserts: 49 | - approx(A.views, B.expected, B.threshold) where A.country == B.country && B.continent != "as" 50 | ... 51 | -------------------------------------------------------------------------------- /src/test/resources/rest-tests/sample.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rest test 3 | description: Testing Validatar 4 | queries: 5 | - name: Query1 6 | engine: rest 7 | value: "function read(input) { 8 | var parsed = JSON.parse(input); 9 | var result = {'users': [], 'counts': []}; 10 | for (var i = 0; i < parsed.length; i++) { 11 | result['users'].push(parsed[i]['name']); 12 | result['counts'].push(parsed[i]['count']); 13 | } 14 | return JSON.stringify(result); 15 | }" 16 | metadata: 17 | - key: url 18 | value: "http://localhost:13412/api/users" 19 | - key: method 20 | value: POST 21 | - key: body 22 | value: "{\"key\": \"value\"}" 23 | - key: function 24 | value: read 25 | - key: some-header 26 | value: header value 27 | - name: Query2 28 | engine: rest 29 | value: "function process(input) { 30 | return JSON.stringify({'max': [14]}); 31 | }" 32 | metadata: 33 | - key: url 34 | value: "http://localhost:13412/api/visits/max" 35 | - key: timeout 36 | value: 5000 37 | - key: retry 38 | value: 3 39 | - name: Failer 40 | engine: rest 41 | value: "" 42 | metadata: 43 | - key: url 44 | value: "http://localhost:13412/api/users" 45 | - key: method 46 | value: PUT 47 | tests: 48 | - name: Test1 49 | description: Why I am testing this. 50 | asserts: 51 | - Query1.users == "admin" 52 | - Query1.count > 10 53 | - name: Test2 54 | description: Why I am testing this. 55 | asserts: 56 | - Query2.count + Query1.count < 50 57 | ... 58 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Query.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import lombok.Getter; 8 | 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | 14 | public class Query extends Executable { 15 | public String name; 16 | public String engine; 17 | public String value; 18 | public List metadata; 19 | @Getter 20 | public int priority = Integer.MAX_VALUE; 21 | 22 | private Result result = null; 23 | 24 | /** 25 | * Add a failure message and mark as failed. 26 | * 27 | * @param failedMessage A {@link java.lang.String} message to set. 28 | */ 29 | public void setFailure(String failedMessage) { 30 | setFailed(); 31 | addMessage(failedMessage); 32 | } 33 | 34 | /** 35 | * Initialize the results. 36 | * 37 | * @return The created {@link com.yahoo.validatar.common.Result} object. 38 | */ 39 | public Result createResults() { 40 | result = new Result(name); 41 | return result; 42 | } 43 | 44 | /** 45 | * Get the results of the query. 46 | * 47 | * @return The {@link com.yahoo.validatar.common.Result} result object. 48 | */ 49 | public Result getResult() { 50 | return result; 51 | } 52 | 53 | /** 54 | * Returns the metadata list flattened into a map. If there are metadata with 55 | * the same key, the last one is one that is kept. 56 | * 57 | * @return A {@link java.util.Map} view of the metadata. 58 | */ 59 | public Map getMetadata() { 60 | if (metadata == null) { 61 | return null; 62 | } 63 | Map map = new HashMap<>(); 64 | // default Collectors.toMap doesn't handle null values 65 | metadata.forEach(m -> map.put(m.key, m.value)); 66 | return map; 67 | } 68 | 69 | /** 70 | * Gets a key from the metadata or returns an {@link Optional#empty()} otherwise. 71 | * 72 | * @param metadata The {@link Metadata} of a {@link Query} viewed as a {@link Map}. 73 | * @param key The key to get from the metadata. 74 | * @return The {@link Optional} value of the key. 75 | */ 76 | public static Optional getKey(Map metadata, String key) { 77 | if (metadata == null) { 78 | return Optional.empty(); 79 | } 80 | String value = metadata.get(key); 81 | return value == null || value.isEmpty() ? Optional.empty() : Optional.of(value); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/report/email/TestSuiteModel.java: -------------------------------------------------------------------------------- 1 | package com.yahoo.validatar.report.email; 2 | 3 | import com.yahoo.validatar.common.Query; 4 | import com.yahoo.validatar.common.Test; 5 | import com.yahoo.validatar.common.TestSuite; 6 | 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | /** 11 | * This class holds the necessary information for rendering 12 | * the Validatar report using Jtwig. 13 | */ 14 | public class TestSuiteModel { 15 | /** 16 | * Test suite name. 17 | */ 18 | public final String name; 19 | /** 20 | * Number of passed queries. 21 | */ 22 | public final int queryPassed; 23 | /** 24 | * Total number of queries. 25 | */ 26 | public final int queryTotal; 27 | /** 28 | * Number of passed tests. 29 | */ 30 | public final int testPassed; 31 | /** 32 | * Total number of tests. 33 | */ 34 | public final int testTotal; 35 | /** 36 | * Number of warned tests. 37 | */ 38 | public final int testWarn; 39 | 40 | /** 41 | * List of failed queries. 42 | */ 43 | public final List failedQueries; 44 | /** 45 | * List of failed tests. 46 | */ 47 | public final List failedTests; 48 | /** 49 | * List of warn tests. 50 | */ 51 | public final List warnTests; 52 | 53 | /** 54 | * Create a {@code TestSuiteModel} from a {@code TestSuite}. 55 | * The constructor will pull the required information from 56 | * the given test suite. 57 | * 58 | * @param testSuite The test suite object to model. 59 | */ 60 | protected TestSuiteModel(TestSuite testSuite) { 61 | failedQueries = new LinkedList<>(); 62 | failedTests = new LinkedList<>(); 63 | warnTests = new LinkedList<>(); 64 | this.name = testSuite.name; 65 | int passCount = 0; 66 | for (Query query : testSuite.queries) { 67 | if (query.failed()) { 68 | failedQueries.add(query); 69 | } else { 70 | passCount++; 71 | } 72 | } 73 | this.queryPassed = passCount; 74 | this.queryTotal = testSuite.queries.size(); 75 | int warnCount = 0; 76 | passCount = 0; 77 | for (Test test : testSuite.tests) { 78 | if (!test.passed()) { 79 | failedTests.add(test); 80 | } else if (test.isWarnOnly()) { 81 | warnTests.add(test); 82 | warnCount++; 83 | } else { 84 | passCount++; 85 | } 86 | } 87 | this.testPassed = passCount; 88 | this.testWarn = warnCount; 89 | this.testTotal = testSuite.tests.size(); 90 | } 91 | 92 | /** 93 | * @return True if all queries and tests passed. 94 | */ 95 | protected boolean allPassed() { 96 | return queryPassed == queryTotal && (testPassed + testWarn) == testTotal; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/report/junit/JUnitFormatterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.report.junit; 6 | 7 | import com.yahoo.validatar.common.Query; 8 | import com.yahoo.validatar.common.TestSuite; 9 | import com.yahoo.validatar.parse.ParseManager; 10 | import org.dom4j.Document; 11 | import org.dom4j.DocumentException; 12 | import org.dom4j.DocumentHelper; 13 | import org.dom4j.util.NodeComparator; 14 | import org.testng.Assert; 15 | import org.testng.annotations.Test; 16 | 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.nio.file.Files; 20 | import java.nio.file.Paths; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | public class JUnitFormatterTest { 26 | @Test 27 | public void testWriteReport() throws IOException, DocumentException { 28 | Map paramMap = new HashMap<>(); 29 | paramMap.put("DATE", "20140807"); 30 | 31 | ParseManager manager = new ParseManager(new String[0]); 32 | 33 | List testSuites = manager.load((new File("src/test/resources/sample-tests/"))); 34 | ParseManager.deParametrize(testSuites, paramMap); 35 | 36 | Assert.assertEquals(testSuites.size(), 3); 37 | 38 | TestSuite simpleExamples = testSuites.stream().filter(s -> "Simple examples".equals(s.name)).findFirst().get(); 39 | Query failingQuery = simpleExamples.queries.get(2); 40 | failingQuery.setFailure("Query had a typo"); 41 | com.yahoo.validatar.common.Test test = simpleExamples.tests.get(1); 42 | test.setFailed(); 43 | test.addMessage("Sample fail message"); 44 | 45 | TestSuite validatarExamples = testSuites.stream().filter(s -> "Validatar Example".equals(s.name)).findFirst().get(); 46 | test = validatarExamples.tests.get(0); 47 | test.setFailed(); 48 | test.addMessage("Another multiline \nfail \nmessage"); 49 | 50 | // Generate the test report file 51 | String[] args = {"--report-file", "target/JUnitOutputTest.xml"}; 52 | JUnitFormatter jUnitFormatter = new JUnitFormatter(); 53 | jUnitFormatter.setup(args); 54 | jUnitFormatter.writeReport(testSuites); 55 | 56 | // Diff the test report file, and the expected output 57 | String output = new String(Files.readAllBytes(Paths.get("target/JUnitOutputTest.xml"))); 58 | Document outputDOM = DocumentHelper.parseText(output); 59 | 60 | String expectedOutput = new String(Files.readAllBytes(Paths.get("src/test/resources/ExpectedJUnitOutput.xml"))); 61 | Document expectedOutputDom = DocumentHelper.parseText(expectedOutput); 62 | 63 | NodeComparator comparator = new NodeComparator(); 64 | if (comparator.compare(expectedOutputDom, outputDOM) != 0) { 65 | Assert.fail("The generated XML does not match expected XML!"); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/parse/yaml/YAMLTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.parse.yaml; 6 | 7 | import com.yahoo.validatar.common.Query; 8 | import com.yahoo.validatar.common.TestSuite; 9 | import org.testng.Assert; 10 | import org.testng.annotations.Test; 11 | 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.FileNotFoundException; 15 | import java.util.Map; 16 | 17 | public class YAMLTest { 18 | private final YAML yaml = new YAML(); 19 | 20 | @Test 21 | public void testLoadOfValidTestFile() throws FileNotFoundException { 22 | TestSuite testSuite = yaml.parse(new FileInputStream(new File("src/test/resources/sample-tests/tests.yaml"))); 23 | Assert.assertEquals(testSuite.name, "Validatar Example"); 24 | } 25 | 26 | @Test 27 | public void testParamNotReplaced() throws FileNotFoundException { 28 | TestSuite testSuite = yaml.parse(new FileInputStream(new File("src/test/resources/sample-tests/tests.yaml"))); 29 | Query query = testSuite.queries.get(2); 30 | Assert.assertEquals(query.value, "TEST ${DATE}"); 31 | } 32 | 33 | @Test 34 | public void testQueryMetadata() throws FileNotFoundException { 35 | TestSuite testSuite = yaml.parse(new FileInputStream(new File("src/test/resources/metadata-tests/tests.yaml"))); 36 | Assert.assertEquals(testSuite.queries.size(), 3); 37 | 38 | Query noMeta = testSuite.queries.get(0); 39 | Query windowMeta = testSuite.queries.get(1); 40 | Query threadMeta = testSuite.queries.get(2); 41 | 42 | Assert.assertNull(noMeta.metadata); 43 | Assert.assertEquals(windowMeta.metadata.size(), 2); 44 | Assert.assertEquals(threadMeta.metadata.size(), 1); 45 | 46 | Map metaMap = noMeta.getMetadata(); 47 | Assert.assertNull(metaMap); 48 | 49 | metaMap = windowMeta.getMetadata(); 50 | Assert.assertEquals(metaMap.size(), 2); 51 | Assert.assertEquals(metaMap.get("windowSize"), "600"); 52 | Assert.assertEquals(metaMap.get("records"), "10000"); 53 | 54 | metaMap = threadMeta.getMetadata(); 55 | Assert.assertEquals(metaMap.size(), 1); 56 | Assert.assertEquals(metaMap.get("threads"), "4"); 57 | } 58 | 59 | @Test 60 | public void testQueryPriority() throws FileNotFoundException { 61 | TestSuite testSuite = yaml.parse(new FileInputStream(new File("src/test/resources/priority-tests/tests.yaml"))); 62 | Assert.assertEquals(testSuite.queries.size(), 5); 63 | Assert.assertEquals(testSuite.queries.get(0).priority, Integer.MAX_VALUE); 64 | Assert.assertEquals(testSuite.queries.get(1).priority, -1); 65 | Assert.assertEquals(testSuite.queries.get(2).priority, 0); 66 | Assert.assertEquals(testSuite.queries.get(3).priority, 1); 67 | Assert.assertEquals(testSuite.queries.get(4).priority, 2); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Pluggable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import joptsimple.OptionParser; 8 | import joptsimple.OptionSet; 9 | import lombok.Getter; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import java.util.HashSet; 13 | import java.util.List; 14 | import java.util.Objects; 15 | import java.util.Set; 16 | 17 | /** 18 | * A class that can be extended to load or plugin additional classes to a type. For example, extending this 19 | * class in an package that loads engines could let it allow loading additional engines at runtime from arguments. 20 | * It only works with classes that can be instantiated with the default constructor. 21 | * 22 | * @param The super type of the pluggable classes. 23 | */ 24 | @Slf4j 25 | public class Pluggable { 26 | @Getter 27 | private OptionParser pluginOptionsParser; 28 | private List> defaults; 29 | private String optionsKey; 30 | 31 | /** 32 | * The constructor. 33 | * 34 | * @param defaults The List of default classes to use as plugins. 35 | * @param key The key to use to load the plugin class from command line arguments. 36 | * @param description A helpful description to provide for what these plugins are. 37 | */ 38 | public Pluggable(List> defaults, String key, String description) { 39 | Objects.requireNonNull(defaults); 40 | pluginOptionsParser = new OptionParser() { 41 | { 42 | accepts(key, description) 43 | .withRequiredArg() 44 | .describedAs("Additional custom fully qualified classes to plug in"); 45 | allowsUnrecognizedOptions(); 46 | } 47 | }; 48 | this.defaults = defaults; 49 | this.optionsKey = key; 50 | } 51 | 52 | /** 53 | * Returns a set view of the instantiated plugins that could be created. 54 | * @param arguments The commandline arguments containing the optional plugin arguments and class names. 55 | * @return A Set of all the instantiated plugin classes. 56 | */ 57 | public Set getPlugins(String[] arguments) { 58 | OptionSet options = pluginOptionsParser.parse(arguments); 59 | Set> pluginClasses = new HashSet<>(defaults); 60 | for (String pluggable : (List) options.valuesOf(optionsKey)) { 61 | try { 62 | Class plugin = (Class) Class.forName(pluggable); 63 | pluginClasses.add(plugin); 64 | } catch (ClassNotFoundException e) { 65 | log.error("Requested plugin class not found: {}", pluggable, e); 66 | } 67 | } 68 | 69 | Set plugins = new HashSet<>(); 70 | for (Class pluginClass : pluginClasses) { 71 | try { 72 | plugins.add(pluginClass.newInstance()); 73 | } catch (InstantiationException ie) { 74 | log.error("Error instantiating {} plugin.\n{}", pluginClass, ie); 75 | } catch (IllegalAccessException iae) { 76 | log.error("Illegal access while loading {} plugin.\n{}", pluginClass, iae); 77 | } 78 | } 79 | return plugins; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/common/PluggableTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import lombok.Getter; 8 | import org.testng.Assert; 9 | import org.testng.annotations.Test; 10 | 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.Set; 14 | 15 | public class PluggableTest { 16 | public static class NormalClassTest { 17 | @Getter 18 | public int count = 0; 19 | 20 | public NormalClassTest() { 21 | count = 1; 22 | } 23 | } 24 | 25 | public static class UninstantiableTest extends NormalClassTest { 26 | public UninstantiableTest() throws InstantiationException { 27 | throw new InstantiationException(); 28 | } 29 | } 30 | 31 | public static class IllegalAccessTest extends NormalClassTest { 32 | public IllegalAccessTest() throws IllegalAccessException { 33 | throw new IllegalAccessException(); 34 | } 35 | } 36 | 37 | public static class NormalSubClassTest extends NormalClassTest { 38 | public NormalSubClassTest() { 39 | super(); 40 | count = 10; 41 | } 42 | } 43 | 44 | public static class AnotherNormalSubClassTest extends NormalClassTest { 45 | public AnotherNormalSubClassTest() { 46 | super(); 47 | count = 32; 48 | } 49 | } 50 | 51 | @Test 52 | public void testPluginDefaults() { 53 | Pluggable pluggable = new Pluggable<>(Arrays.asList(NormalSubClassTest.class, 54 | AnotherNormalSubClassTest.class), 55 | "key", ""); 56 | Assert.assertNotNull(pluggable.getPluginOptionsParser()); 57 | Set plugins = pluggable.getPlugins(new String[0]); 58 | Assert.assertEquals(plugins.size(), 2); 59 | int sum = 0; 60 | for (NormalClassTest item : plugins) { 61 | sum += item.getCount(); 62 | } 63 | Assert.assertEquals(sum, 42); 64 | } 65 | 66 | @Test 67 | public void testIllegalAccessClass() { 68 | Pluggable pluggable = new Pluggable<>(Arrays.asList(IllegalAccessTest.class), "key", ""); 69 | Set plugins = pluggable.getPlugins(new String[0]); 70 | Assert.assertEquals(plugins.size(), 0); 71 | } 72 | 73 | @Test 74 | public void testUninstantiableClass() { 75 | Pluggable pluggable = new Pluggable<>(Arrays.asList(UninstantiableTest.class), "key", ""); 76 | Set plugins = pluggable.getPlugins(new String[0]); 77 | Assert.assertEquals(plugins.size(), 0); 78 | } 79 | 80 | @Test 81 | public void testUnknownClass() { 82 | Pluggable pluggable = new Pluggable<>(Collections.emptyList(), "custom", ""); 83 | String[] arguments = { "--custom", "foo.bar.FakeClass" }; 84 | Set plugins = pluggable.getPlugins(arguments); 85 | Assert.assertEquals(plugins.size(), 0); 86 | } 87 | 88 | @Test 89 | public void testArgumentClassLoading() { 90 | Pluggable pluggable = new Pluggable<>(Collections.emptyList(), "custom", ""); 91 | String[] arguments = { "--custom", "com.yahoo.validatar.FakeTestClass", 92 | "--custom", "com.yahoo.validatar.FakeTestClass" }; 93 | Set plugins = pluggable.getPlugins(arguments); 94 | Assert.assertEquals(plugins.size(), 1); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/assertion/Assertor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.assertion; 6 | 7 | import com.yahoo.validatar.common.Column; 8 | import com.yahoo.validatar.common.Result; 9 | import com.yahoo.validatar.common.Test; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.antlr.v4.runtime.CharStream; 12 | import org.antlr.v4.runtime.CharStreams; 13 | import org.antlr.v4.runtime.CommonTokenStream; 14 | 15 | import java.util.List; 16 | import java.util.Set; 17 | 18 | @Slf4j 19 | public class Assertor { 20 | public static final String RESULT_COLUMN = ""; 21 | /** 22 | * Takes a Results object and a List of Test, performs the assertions and updates the Tests with the results. 23 | * 24 | * @param results A {@link List} of {@link Result} object containing the results of the queries. 25 | * @param tests A {@link List} of {@link Test} using these results. 26 | */ 27 | public static void assertAll(List results, List tests) { 28 | tests.stream().forEach(t -> checkAssertions(results, t)); 29 | } 30 | 31 | private static void checkAssertions(List results, Test test) { 32 | List assertions = test.asserts; 33 | // Check for invalid input 34 | if (assertions == null || assertions.size() == 0) { 35 | test.setFailed(); 36 | test.addMessage("No assertion was provided!"); 37 | return; 38 | } 39 | AssertVisitor visitor = new AssertVisitor(results); 40 | assertions.stream().forEach(a -> checkAssertion(a, visitor, test)); 41 | } 42 | 43 | private static void checkAssertion(String assertion, AssertVisitor visitor, Test test) { 44 | log.info("Running assertion: {}", assertion); 45 | try { 46 | CharStream in = CharStreams.fromString(assertion); 47 | GrammarLexer lexer = new GrammarLexer(in); 48 | CommonTokenStream tokens = new CommonTokenStream(lexer); 49 | GrammarParser parser = new GrammarParser(tokens); 50 | 51 | Expression expression = visitor.visit(parser.statement()); 52 | // This expression will evaluate to a boolean Column of true or false TypedObjects. It needs no data. 53 | Column result = expression.evaluate(); 54 | 55 | if (hasFailures(result)) { 56 | Set columnsSeen = visitor.getSeenIdentifiers(); 57 | Result joined = visitor.getJoinedResult(); 58 | Result releventData = Result.copy(joined, columnsSeen); 59 | releventData.addQualifiedColumn(RESULT_COLUMN, result); 60 | 61 | String assertionMessage = "Assertion " + assertion + " was false"; 62 | String resultsMessage = "Result had false values: " + result; 63 | String columnsMessage = "Examined columns: " + columnsSeen; 64 | String relevantColumnsMessage = "Relevant column data used: \n" + releventData.prettyPrint(); 65 | String dataMessage = "All Result data used: \n" + joined.prettyPrint(); 66 | 67 | test.setFailed(); 68 | test.addMessage(assertionMessage); 69 | test.addMessage(resultsMessage); 70 | test.addMessage(columnsMessage); 71 | test.addMessage(dataMessage); 72 | test.addMessage(relevantColumnsMessage); 73 | log.info("{}\n{}\n{}\n{}\n{}\n", assertionMessage, resultsMessage, columnsMessage, dataMessage, relevantColumnsMessage); 74 | } 75 | } catch (Exception e) { 76 | test.setFailed(); 77 | String dataMessage = "Data used: \n" + visitor.getJoinedResult().prettyPrint(); 78 | test.addMessage(assertion + " failed with exception: " + e.getMessage()); 79 | test.addMessage(dataMessage); 80 | log.error("Assertion failed with exception", e); 81 | log.error("\n{}", dataMessage); 82 | } finally { 83 | visitor.reset(); 84 | } 85 | } 86 | 87 | private static boolean hasFailures(Column result) { 88 | return result.stream().anyMatch(t -> !((Boolean) t.data)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/assertion/Expression.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.assertion; 6 | 7 | import com.yahoo.validatar.common.Column; 8 | import com.yahoo.validatar.common.Result; 9 | import com.yahoo.validatar.common.TypedObject; 10 | import lombok.RequiredArgsConstructor; 11 | 12 | import java.util.function.Function; 13 | 14 | /** 15 | * This class wraps an tree of Expressions to evaluate later with some context (data). Essentially a State Monad. 16 | */ 17 | @RequiredArgsConstructor 18 | public class Expression { 19 | private final Function expression; 20 | 21 | /** 22 | * Evaluates the expression with the given data result and returns the result. 23 | * 24 | * @param result The {@link Result} that is the data for this expression. 25 | * @return The resulting {@link Column}. 26 | */ 27 | public Column evaluate(Result result) { 28 | return expression.apply(result); 29 | } 30 | 31 | /** 32 | * Evaluates the expression with the no data context and returns the result. 33 | * 34 | * @return The resulting {@link Column}. 35 | */ 36 | public Column evaluate() { 37 | return expression.apply(null); 38 | } 39 | 40 | /** 41 | * Composes a {@link UnaryColumnOperation} onto a given {@link Expression}. Monadic lift. 42 | * 43 | * @param operation The operation to chain onto. 44 | * @param expression The expression to apply the operation on. 45 | * @return The new expression. 46 | */ 47 | public static Expression compose(UnaryColumnOperation operation, Expression expression) { 48 | return new Expression(data -> operation.apply(expression.evaluate(data))); 49 | } 50 | 51 | /** 52 | * Composes a {@link BinaryColumnOperation} onto two given {@link Expression}. Monadic lift. 53 | * 54 | * @param operation The operation to chain onto. 55 | * @param a The first expression to apply the operation on. 56 | * @param b The second expression to apply the operation on. 57 | * @return The new expression. 58 | */ 59 | public static Expression compose(BinaryColumnOperation operation, Expression a, Expression b) { 60 | return new Expression(data -> operation.apply(a.evaluate(data), b.evaluate(data))); 61 | } 62 | 63 | /** 64 | * Wraps a {@link Column} as an Expression. Evaluating that returns the Column. Monadic unit. 65 | * 66 | * @param input The Column to wrap. 67 | * @return An Expression. 68 | */ 69 | public static Expression wrap(Column input) { 70 | return new Expression(d -> input); 71 | } 72 | 73 | /** 74 | * Wraps a {@link TypedObject} as an Expression. Evaluating that returns it as a scalar Column. Monadic unit 75 | * for scalar Column. 76 | * 77 | * @param input The TypedObject to wrap. 78 | * @return An Expression. 79 | */ 80 | public static Expression wrap(TypedObject input) { 81 | return wrap(new Column(input)); 82 | } 83 | 84 | /* 85 | ******************************************************************************** 86 | * Helper functional interfaces to simplify typing * 87 | ******************************************************************************** 88 | */ 89 | 90 | @FunctionalInterface 91 | public interface UnaryColumnOperation { 92 | /** 93 | * Applies the operation on the given {@link Column}. 94 | * 95 | * @param input The column to apply the operation on. 96 | * @return The resulting column. 97 | */ 98 | Column apply(Column input); 99 | } 100 | 101 | @FunctionalInterface 102 | public interface BinaryColumnOperation { 103 | /** 104 | * Applies the operation on the given {@link Column}. 105 | * 106 | * @param a The first column to apply the operation on. 107 | * @param b The second column to apply the operation on. 108 | * @return The resulting column. 109 | */ 110 | Column apply(Column a, Column b); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/report/junit/JUnitFormatter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.report.junit; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | import com.yahoo.validatar.common.Query; 9 | import com.yahoo.validatar.common.Test; 10 | import com.yahoo.validatar.common.TestSuite; 11 | import com.yahoo.validatar.report.Formatter; 12 | import joptsimple.OptionParser; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.apache.commons.lang3.StringUtils; 15 | import org.dom4j.Document; 16 | import org.dom4j.DocumentHelper; 17 | import org.dom4j.Element; 18 | import org.dom4j.io.OutputFormat; 19 | import org.dom4j.io.XMLWriter; 20 | 21 | import java.io.FileWriter; 22 | import java.io.IOException; 23 | import java.util.List; 24 | 25 | @Slf4j 26 | public class JUnitFormatter implements Formatter { 27 | public static final String REPORT_FILE = "report-file"; 28 | 29 | public static final String JUNIT = "junit"; 30 | 31 | public static final String TESTSUITES_TAG = "testsuites"; 32 | public static final String TESTSUITE_TAG = "testsuite"; 33 | public static final String TESTCASE_TAG = "testcase"; 34 | public static final String FAILED_TAG = "failed"; 35 | public static final String SKIPPED_TAG = "skipped"; 36 | public static final String TESTS_ATTRIBUTE = "tests"; 37 | public static final String NAME_ATTRIBUTE = "name"; 38 | 39 | public static final String NEWLINE = "\n"; 40 | 41 | private final OptionParser parser = new OptionParser() { 42 | { 43 | accepts(REPORT_FILE, "File to store the test reports.") 44 | .withRequiredArg() 45 | .describedAs("Report file") 46 | .defaultsTo("report.xml"); 47 | allowsUnrecognizedOptions(); 48 | } 49 | }; 50 | private String outputFile; 51 | 52 | @Override 53 | public boolean setup(String[] arguments) { 54 | outputFile = (String) parser.parse(arguments).valueOf(REPORT_FILE); 55 | return true; 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | * Writes out the report for the given testSuites in the JUnit XML format. 61 | */ 62 | @Override 63 | public void writeReport(List testSuites) throws IOException { 64 | Document document = DocumentHelper.createDocument(); 65 | 66 | Element testSuitesRoot = document.addElement(TESTSUITES_TAG); 67 | 68 | // Output for each test suite 69 | for (TestSuite testSuite : testSuites) { 70 | Element testSuiteRoot = testSuitesRoot.addElement(TESTSUITE_TAG); 71 | testSuiteRoot.addAttribute(TESTS_ATTRIBUTE, Integer.toString(testSuite.queries.size() + testSuite.tests.size())); 72 | testSuiteRoot.addAttribute(NAME_ATTRIBUTE, testSuite.name); 73 | 74 | for (Query query : testSuite.queries) { 75 | Element queryNode = testSuiteRoot.addElement(TESTCASE_TAG).addAttribute(NAME_ATTRIBUTE, query.name); 76 | if (query.failed()) { 77 | String failureMessage = StringUtils.join(query.getMessages(), NEWLINE); 78 | queryNode.addElement(FAILED_TAG).addCDATA(failureMessage); 79 | } 80 | } 81 | for (Test test : testSuite.tests) { 82 | Element testNode = testSuiteRoot.addElement(TESTCASE_TAG).addAttribute(NAME_ATTRIBUTE, test.name); 83 | if (test.failed()) { 84 | Element target = testNode; 85 | if (test.isWarnOnly()) { 86 | testNode.addElement(SKIPPED_TAG); 87 | } else { 88 | target = testNode.addElement(FAILED_TAG); 89 | } 90 | target.addCDATA(NEWLINE + test.description + NEWLINE + StringUtils.join(test.getMessages(), NEWLINE)); 91 | } 92 | } 93 | } 94 | 95 | OutputFormat format = OutputFormat.createPrettyPrint(); 96 | XMLWriter writer = new XMLWriter(new FileWriter(outputFile), format); 97 | writer.write(document); 98 | writer.close(); 99 | } 100 | 101 | @Override 102 | public void printHelp() { 103 | Helpable.printHelp("Junit report options", parser); 104 | } 105 | 106 | @Override 107 | public String getName() { 108 | return JUNIT; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Column.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import lombok.Getter; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Iterator; 11 | import java.util.List; 12 | import java.util.Objects; 13 | import java.util.stream.Stream; 14 | 15 | /** 16 | * Wraps a sequence of {@link TypedObject} as a Column. 17 | */ 18 | public class Column implements Iterable { 19 | @Getter 20 | private final List values; 21 | 22 | /** 23 | * Creates a new empty Column. 24 | */ 25 | public Column() { 26 | values = new ArrayList<>(); 27 | } 28 | 29 | /** 30 | * Wraps a {@link TypedObject} as a Column of size 1. 31 | * 32 | * @param object The single TypedObject to wrap. 33 | */ 34 | public Column(TypedObject object) { 35 | Objects.requireNonNull(object); 36 | values = new ArrayList<>(); 37 | values.add(object); 38 | } 39 | 40 | /** 41 | * Wraps a {@link List} of {@link TypedObject} as a Column. 42 | * 43 | * @param list The TypedObjects to wrap. 44 | */ 45 | public Column(List list) { 46 | Objects.requireNonNull(list); 47 | values = new ArrayList<>(list); 48 | } 49 | 50 | /** 51 | * If this contains no data, then it is empty. 52 | * 53 | * @return A boolean denoting whether this is empty. 54 | */ 55 | public boolean isEmpty() { 56 | return size() == 0; 57 | } 58 | 59 | /** 60 | * If this contains only one {@link TypedObject}, then it is a scalar. 61 | * 62 | * @return A boolean denoting whether this is a scalar. 63 | */ 64 | public boolean isScalar() { 65 | return size() == 1; 66 | } 67 | 68 | /** 69 | * If this contains more than one {@link TypedObject}, then it is a vector. 70 | * 71 | * @return A boolean denoting whether this is a vector. 72 | */ 73 | public boolean isVector() { 74 | return size() > 1; 75 | } 76 | 77 | /** 78 | * The number of elements in the vector. 79 | * 80 | * @return The size of the vector. 81 | */ 82 | public int size() { 83 | return values.size(); 84 | } 85 | 86 | /** 87 | * Returns the first element of this vector. 88 | * 89 | * @return The {@link TypedObject} at the first position. 90 | */ 91 | public TypedObject first() { 92 | return get(0); 93 | } 94 | 95 | /** 96 | * Returns the {@link TypedObject} at the given position. 97 | * 98 | * @param position The integer representing the position to get. 99 | * @return The TypedObject at that position if one exists. 100 | * @throws IndexOutOfBoundsException if the position is invalid. 101 | */ 102 | public TypedObject get(int position) { 103 | if (position < 0 || position >= values.size()) { 104 | throw new IndexOutOfBoundsException("There is no object at position " + position + " in " + this); 105 | } 106 | return values.get(position); 107 | } 108 | 109 | /** 110 | * Add a {@link TypedObject} to this vector. 111 | * 112 | * @param object The non-null TypedObject to add. 113 | */ 114 | public void add(TypedObject object) { 115 | values.add(object); 116 | } 117 | 118 | /** 119 | * Add another {@link Column} to this vector. 120 | * 121 | * @param column The non-null Column to add. 122 | */ 123 | public void add(Column column) { 124 | column.stream().forEach(this::add); 125 | } 126 | 127 | /** 128 | * Creates a full copy of this Column. 129 | * 130 | * @return The copied column. 131 | */ 132 | public Column copy() { 133 | return this.stream().collect(Column::new, (c, t) -> c.add(t == null ? null : new TypedObject(t.data, t.type)), Column::add); 134 | } 135 | 136 | /** 137 | * Creates a {@link Stream} of the values in this Column. 138 | * 139 | * @return A Stream of the {@link TypedObject} in this object. 140 | */ 141 | public Stream stream() { 142 | return values.stream(); 143 | } 144 | 145 | @Override 146 | public Iterator iterator() { 147 | return values.iterator(); 148 | } 149 | 150 | @Override 151 | public String toString() { 152 | return Objects.toString(values); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/antlr4/com/yahoo/validatar/assertion/Grammar.g4: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | 6 | grammar Grammar; 7 | 8 | truthy 9 | : TRUE 10 | | FALSE 11 | ; 12 | 13 | numeric 14 | : w=WholeNumber # wholeNumber 15 | | d=DecimalNumber # decimalNumber 16 | ; 17 | 18 | base 19 | : t=truthy # truthValue 20 | | n=numeric # numericValue 21 | | s=StringLiteral # stringValue 22 | | i=Identifier # identifier 23 | ; 24 | 25 | functionalExpression 26 | : APPROX LEFTPAREN l=base COMMA r=base COMMA p=base RIGHTPAREN # approxValue 27 | ; 28 | 29 | baseExpression 30 | : b=base # baseValue 31 | | f=functionalExpression # functionalValue 32 | | LEFTPAREN o=orExpression RIGHTPAREN # parenthesizedValue 33 | ; 34 | 35 | unaryExpression 36 | : m=MINUS? b=baseExpression # negateValue 37 | | n=NOT? b=baseExpression # logicalNegateValue 38 | ; 39 | 40 | multiplicativeExpression 41 | : u=unaryExpression # baseUnaryValue 42 | | m=multiplicativeExpression TIMES u=unaryExpression # multiplyValue 43 | | m=multiplicativeExpression DIVIDE u=unaryExpression # divideValue 44 | | m=multiplicativeExpression MODULUS u=unaryExpression # modValue 45 | ; 46 | 47 | additiveExpression 48 | : m=multiplicativeExpression # baseMultiplicativeValue 49 | | a=additiveExpression PLUS m=multiplicativeExpression # addValue 50 | | a=additiveExpression MINUS m=multiplicativeExpression # subtractValue 51 | ; 52 | 53 | relationalExpression 54 | : a=additiveExpression # baseAdditiveValue 55 | | r=relationalExpression GREATER a=additiveExpression # greaterValue 56 | | r=relationalExpression LESS a=additiveExpression # lessValue 57 | | r=relationalExpression LESSEQUAL a=additiveExpression # lessEqualValue 58 | | r=relationalExpression GREATEREQUAL a=additiveExpression # greaterEqualValue 59 | ; 60 | 61 | equalityExpression 62 | : r=relationalExpression # baseRelativeValue 63 | | e=equalityExpression EQUAL r=relationalExpression # equalityValue 64 | | e=equalityExpression NOTEQUAL r=relationalExpression # notEqualityValue 65 | ; 66 | 67 | andExpression 68 | : e=equalityExpression # baseEqualityValue 69 | | a=andExpression AND e=equalityExpression # andValue 70 | ; 71 | 72 | orExpression 73 | : a=andExpression # baseAndValue 74 | | o=orExpression OR a=andExpression # orValue 75 | ; 76 | 77 | statement 78 | : o=orExpression # baseOrValue 79 | | o=orExpression WHERE j=orExpression # joinValue 80 | ; 81 | 82 | DOUBLEQUOTE : '"'; 83 | QUOTE : '\''; 84 | PERIOD : '.'; 85 | COMMA : ','; 86 | LEFTPAREN : '('; 87 | RIGHTPAREN : ')'; 88 | PLUS : '+'; 89 | MINUS : '-'; 90 | TIMES : '*'; 91 | DIVIDE : '/'; 92 | MODULUS : '%'; 93 | GREATER : '>'; 94 | LESS : '<'; 95 | GREATEREQUAL : '>='; 96 | LESSEQUAL : '<='; 97 | NOTEQUAL : '!='; 98 | EQUAL : '=='; 99 | NOT : '!'; 100 | AND : '&&'; 101 | OR : '||'; 102 | TRUE : 'true'; 103 | FALSE : 'false'; 104 | WHERE : 'where'; 105 | APPROX : 'approx'; 106 | 107 | Whitespace 108 | : [ \t]+ 109 | -> skip 110 | ; 111 | 112 | Newline 113 | : [\r\n] 114 | -> skip 115 | ; 116 | 117 | fragment 118 | NoDoubleQuoteCharacter 119 | : ~["] 120 | ; 121 | 122 | fragment 123 | NoQuoteCharacter 124 | : ~['] 125 | ; 126 | 127 | fragment 128 | NonDigit 129 | : [a-zA-Z_] 130 | ; 131 | 132 | fragment 133 | Digit 134 | : [0-9] 135 | ; 136 | 137 | fragment 138 | IdentifierCharacter 139 | : NonDigit 140 | | Digit 141 | ; 142 | 143 | WholeNumber 144 | : Digit+ 145 | ; 146 | 147 | DecimalNumber 148 | : Digit+ PERIOD Digit+ 149 | ; 150 | 151 | StringLiteral 152 | : DOUBLEQUOTE NoDoubleQuoteCharacter* DOUBLEQUOTE 153 | | QUOTE NoQuoteCharacter* QUOTE 154 | ; 155 | 156 | Identifier 157 | : IdentifierCharacter+ PERIOD ? IdentifierCharacter+ 158 | ; 159 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/common/ColumnTest.java: -------------------------------------------------------------------------------- 1 | package com.yahoo.validatar.common; 2 | 3 | import com.yahoo.validatar.common.TypeSystem.Type; 4 | import org.testng.Assert; 5 | import org.testng.annotations.Test; 6 | 7 | import java.util.Iterator; 8 | 9 | import static java.util.Arrays.asList; 10 | 11 | public class ColumnTest { 12 | @Test 13 | public void testEmpty() { 14 | Column column = new Column(); 15 | Assert.assertTrue(column.isEmpty()); 16 | Assert.assertFalse(column.isScalar()); 17 | Assert.assertFalse(column.isVector()); 18 | Assert.assertEquals(column.size(), 0); 19 | } 20 | 21 | @Test 22 | public void testWrappingSingleItem() { 23 | Column column = new Column(new TypedObject(42L, Type.LONG)); 24 | Assert.assertFalse(column.isEmpty()); 25 | Assert.assertTrue(column.isScalar()); 26 | Assert.assertFalse(column.isVector()); 27 | Assert.assertEquals(column.size(), 1); 28 | Assert.assertEquals(column.get(0).type, Type.LONG); 29 | Assert.assertEquals(column.get(0).data, 42L); 30 | Assert.assertEquals(column.first().type, Type.LONG); 31 | Assert.assertEquals(column.first().data, 42L); 32 | } 33 | 34 | @Test 35 | public void testWrappingMultipleItem() { 36 | Column column = new Column(asList(new TypedObject(42L, Type.LONG), new TypedObject(84L, Type.LONG))); 37 | Assert.assertFalse(column.isEmpty()); 38 | Assert.assertFalse(column.isScalar()); 39 | Assert.assertTrue(column.isVector()); 40 | Assert.assertEquals(column.size(), 2); 41 | Assert.assertEquals(column.get(0).type, Type.LONG); 42 | Assert.assertEquals(column.get(0).data, 42L); 43 | Assert.assertEquals(column.first().type, Type.LONG); 44 | Assert.assertEquals(column.first().data, 42L); 45 | Assert.assertEquals(column.get(1).type, Type.LONG); 46 | Assert.assertEquals(column.get(1).data, 84L); 47 | } 48 | 49 | @Test(expectedExceptions = IndexOutOfBoundsException.class) 50 | public void testNegativePosition() { 51 | Column column = new Column(); 52 | column.get(-1); 53 | } 54 | 55 | @Test(expectedExceptions = IndexOutOfBoundsException.class) 56 | public void testOutOfRangePosition() { 57 | Column column = new Column(); 58 | column.get(0); 59 | } 60 | 61 | @Test 62 | public void testAddition() { 63 | Column column = new Column(); 64 | column.add(new TypedObject(42L, Type.LONG)); 65 | column.add(new TypedObject(84L, Type.LONG)); 66 | 67 | Assert.assertEquals(column.size(), 2); 68 | 69 | Column another = new Column(asList(new TypedObject(1L, Type.LONG), new TypedObject(2L, Type.LONG))); 70 | column.add(another); 71 | Assert.assertEquals(column.size(), 4); 72 | 73 | Assert.assertEquals(column.get(2).type, Type.LONG); 74 | Assert.assertEquals(column.get(2).data, 1L); 75 | Assert.assertEquals(column.get(3).type, Type.LONG); 76 | Assert.assertEquals(column.get(3).data, 2L); 77 | } 78 | 79 | @Test 80 | public void testCopy() { 81 | Column column = new Column(); 82 | column.add(new TypedObject(42L, Type.LONG)); 83 | column.add(new TypedObject(84L, Type.LONG)); 84 | column.add((TypedObject) null); 85 | 86 | Column copy = column.copy(); 87 | 88 | Assert.assertEquals(column.get(1).type, Type.LONG); 89 | Assert.assertEquals(column.get(1).data, 84L); 90 | Assert.assertEquals(copy.get(1).type, Type.LONG); 91 | Assert.assertEquals(copy.get(1).data, 84L); 92 | Assert.assertNull(copy.get(2)); 93 | 94 | Assert.assertFalse(column.get(1) == copy.get(1)); 95 | } 96 | 97 | @Test 98 | public void testIteration() { 99 | Column column = new Column(); 100 | column.add(new TypedObject(42L, Type.LONG)); 101 | column.add(new TypedObject(84L, Type.LONG)); 102 | 103 | Long sum = column.stream().map(a -> (Long) a.data).reduce(0L, (a, b) -> a + b); 104 | Assert.assertEquals(sum, Long.valueOf(126)); 105 | 106 | sum = 0L; 107 | Iterator iterator = column.iterator(); 108 | while (iterator.hasNext()) { 109 | sum += (Long) iterator.next().data; 110 | } 111 | Assert.assertEquals(sum, Long.valueOf(126)); 112 | } 113 | 114 | @Test 115 | public void testToString() { 116 | Column column = new Column(); 117 | column.add(new TypedObject(42L, Type.LONG)); 118 | column.add(new TypedObject(84L, Type.LONG)); 119 | 120 | Assert.assertEquals(column.toString(), "[<42, LONG>, <84, LONG>]"); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/TestHelpers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar; 6 | 7 | import com.yahoo.validatar.common.Column; 8 | import com.yahoo.validatar.common.Query; 9 | import com.yahoo.validatar.common.Result; 10 | import com.yahoo.validatar.common.TestSuite; 11 | import com.yahoo.validatar.common.TypeSystem; 12 | import com.yahoo.validatar.common.TypedObject; 13 | import com.yahoo.validatar.parse.yaml.YAML; 14 | 15 | import java.io.File; 16 | import java.io.FileInputStream; 17 | import java.io.FileNotFoundException; 18 | import java.math.BigDecimal; 19 | import java.sql.Timestamp; 20 | import java.util.ArrayList; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | public class TestHelpers { 26 | public static Query getQueryFrom(String file, String name) throws FileNotFoundException { 27 | return getTestSuiteFrom(file).queries.stream().filter(q -> name.equals(q.name)).findAny().get(); 28 | } 29 | 30 | public static TestSuite getTestSuiteFrom(String file) throws FileNotFoundException { 31 | File testFile = new File(TestHelpers.class.getClassLoader().getResource(file).getFile()); 32 | return new YAML().parse(new FileInputStream(testFile)); 33 | } 34 | 35 | public static List wrap(T... data) { 36 | List asList = new ArrayList<>(); 37 | Collections.addAll(asList, data); 38 | return asList; 39 | } 40 | 41 | public static boolean boolify(TypedObject type) { 42 | return (Boolean) type.data; 43 | } 44 | 45 | public static boolean boolify(Column data) { 46 | for (TypedObject object : data) { 47 | if (!boolify(object)) { 48 | return false; 49 | } 50 | } 51 | return true; 52 | } 53 | 54 | public static TypedObject getTyped(TypeSystem.Type type, Object value) { 55 | if (value == null) { 56 | return null; 57 | } 58 | switch (type) { 59 | case STRING: 60 | return new TypedObject((String) value, TypeSystem.Type.STRING); 61 | case LONG: 62 | return new TypedObject((Long) value, TypeSystem.Type.LONG); 63 | case DOUBLE: 64 | return new TypedObject((Double) value, TypeSystem.Type.DOUBLE); 65 | case DECIMAL: 66 | return new TypedObject((BigDecimal) value, TypeSystem.Type.DECIMAL); 67 | case TIMESTAMP: 68 | return new TypedObject((Timestamp) value, TypeSystem.Type.TIMESTAMP); 69 | case BOOLEAN: 70 | return new TypedObject((Boolean) value, TypeSystem.Type.BOOLEAN); 71 | default: 72 | throw new RuntimeException("Unknown type"); 73 | } 74 | } 75 | 76 | public static Column asColumn(TypeSystem.Type type, Object... data) { 77 | Column column = new Column(); 78 | for (Object object : data) { 79 | column.add(getTyped(type, object)); 80 | } 81 | return column; 82 | } 83 | 84 | public static boolean isEqual(TypedObject actual, TypedObject expected) { 85 | if (actual == null && expected == null) { 86 | return true; 87 | } 88 | if (actual == null ^ expected == null) { 89 | return false; 90 | } 91 | if (expected.type != actual.type) { 92 | return false; 93 | } 94 | return expected.data.compareTo(actual.data) == 0; 95 | } 96 | 97 | public static boolean isEqual(Column actual, Column expected) { 98 | if (actual == null && expected == null) { 99 | return true; 100 | } 101 | if (actual == null ^ expected == null) { 102 | return false; 103 | } 104 | if (actual.size() != expected.size()) { 105 | return false; 106 | } 107 | for (int i = 0; i < expected.size(); ++i) { 108 | if (!isEqual(actual.get(i), expected.get(i))) { 109 | return false; 110 | } 111 | } 112 | return true; 113 | } 114 | 115 | public static boolean isEqual(Result actual, Result expected) { 116 | if (actual == null && expected == null) { 117 | return true; 118 | } 119 | if (actual == null ^ expected == null) { 120 | return false; 121 | } 122 | if (!expected.getNamespace().equals(actual.getNamespace())) { 123 | return false; 124 | } 125 | if (expected.numberOfRows() != actual.numberOfRows()) { 126 | return false; 127 | } 128 | Map expectedData = expected.getColumns(); 129 | Map actualData = actual.getColumns(); 130 | if (expectedData.keySet().size() != actualData.keySet().size()) { 131 | return false; 132 | } 133 | return expectedData.entrySet().stream().allMatch(e -> isEqual(actualData.get(e.getKey()), e.getValue())); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/parse/ParseManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.parse; 6 | 7 | import com.yahoo.validatar.common.Metadata; 8 | import com.yahoo.validatar.common.Query; 9 | import com.yahoo.validatar.common.TestSuite; 10 | import org.mockito.Mockito; 11 | import org.testng.Assert; 12 | import org.testng.annotations.Test; 13 | 14 | import java.io.File; 15 | import java.util.Arrays; 16 | import java.util.Collections; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | public class ParseManagerTest { 22 | @Test 23 | public void testFailLoadOfNonFile() { 24 | ParseManager manager = new ParseManager(new String[0]); 25 | TestSuite testSuite = manager.getTestSuite(new File("src/test/resources")); 26 | Assert.assertNull(testSuite); 27 | } 28 | 29 | @Test 30 | public void testFailLoadOfUnknownFile() { 31 | ParseManager manager = new ParseManager(new String[0]); 32 | TestSuite testSuite = manager.getTestSuite(new File("src/test/resources/log4j.properties")); 33 | Assert.assertNull(testSuite); 34 | } 35 | 36 | @Test 37 | public void testFailLoadOfBadExtensionFile() { 38 | ParseManager manager = new ParseManager(new String[0]); 39 | TestSuite testSuite = manager.getTestSuite(new File("src/test/resources/log4j.properties")); 40 | Assert.assertNull(testSuite); 41 | } 42 | 43 | @Test 44 | public void testFailLoadOfNoExtensionFile() { 45 | ParseManager manager = new ParseManager(new String[0]); 46 | TestSuite testSuite = manager.getTestSuite(new File("LICENSE")); 47 | Assert.assertNull(testSuite); 48 | } 49 | 50 | @Test 51 | public void testFailLoadOfDisappearingFile() { 52 | ParseManager manager = new ParseManager(new String[0]); 53 | File mocked = Mockito.mock(File.class); 54 | Mockito.when(mocked.isFile()).thenReturn(true); 55 | Mockito.when(mocked.getName()).thenReturn("foo.yaml"); 56 | Mockito.when(mocked.getPath()).thenReturn(null); 57 | TestSuite testSuite = manager.getTestSuite(mocked); 58 | Assert.assertNull(testSuite); 59 | } 60 | 61 | @Test 62 | public void testFailLoadOfNullPath() { 63 | ParseManager manager = new ParseManager(new String[0]); 64 | List loaded = manager.load(null); 65 | Assert.assertTrue(loaded.isEmpty()); 66 | } 67 | 68 | @Test 69 | public void testFailLoadOfEmptyDirectory() { 70 | ParseManager manager = new ParseManager(new String[0]); 71 | File mocked = Mockito.mock(File.class); 72 | Mockito.when(mocked.isFile()).thenReturn(false); 73 | Mockito.when(mocked.listFiles()).thenReturn(null); 74 | List loaded = manager.load(mocked); 75 | Assert.assertTrue(loaded.isEmpty()); 76 | } 77 | 78 | @Test 79 | public void testLoadOfDirectory() { 80 | ParseManager manager = new ParseManager(new String[0]); 81 | List loaded = manager.load(new File("src/test/resources/sample-tests")); 82 | Assert.assertEquals(loaded.size(), 3); 83 | } 84 | 85 | @Test 86 | public void testFailExpansion() { 87 | Query query = new Query(); 88 | query.value = "${var}"; 89 | ParseManager.deParametrize(query, Collections.emptyMap()); 90 | Assert.assertEquals(query.value, "${var}"); 91 | } 92 | 93 | @Test 94 | public void testExpansion() { 95 | Query query = new Query(); 96 | query.value = "${var}"; 97 | ParseManager.deParametrize(query, Collections.singletonMap("var", "foo")); 98 | Assert.assertEquals(query.value, "foo"); 99 | } 100 | 101 | @Test 102 | public void testMetadataExpansion() { 103 | Query query = new Query(); 104 | Metadata metadataOne = new Metadata("${foo}", "${bar}"); 105 | Metadata metadataTwo = new Metadata("x", "${baz}"); 106 | Map parameters = new HashMap<>(); 107 | parameters.put("foo", "1"); 108 | parameters.put("bar", "2"); 109 | parameters.put("baz", "3"); 110 | parameters.put("var", "4"); 111 | query.value = "${var}"; 112 | query.metadata = Arrays.asList(metadataOne, metadataTwo); 113 | ParseManager.deParametrize(query, parameters); 114 | Assert.assertEquals(query.metadata.get(0).key, "1"); 115 | Assert.assertEquals(query.metadata.get(0).value, "2"); 116 | Assert.assertEquals(query.metadata.get(1).key, "x"); 117 | Assert.assertEquals(query.metadata.get(1).value, "3"); 118 | Assert.assertEquals(query.value, "4"); 119 | } 120 | 121 | @Test 122 | public void testTestAssertExpansion() { 123 | com.yahoo.validatar.common.Test test = new com.yahoo.validatar.common.Test(); 124 | Map parameters = new HashMap<>(); 125 | parameters.put("foo", "1"); 126 | parameters.put("bar", "2"); 127 | parameters.put("baz", "3"); 128 | parameters.put("var", "4"); 129 | test.asserts = Arrays.asList("${foo} != QUERY.column", "${bar} > 1 && approx(${foo}, QUERY.column, ${var})"); 130 | ParseManager.deParametrize(test, parameters); 131 | Assert.assertEquals(test.asserts.get(0), "1 != QUERY.column"); 132 | Assert.assertEquals(test.asserts.get(1), "2 > 1 && approx(1, QUERY.column, 4)"); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/report/FormatManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.report; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | import com.yahoo.validatar.common.Pluggable; 9 | import com.yahoo.validatar.common.TestSuite; 10 | import com.yahoo.validatar.report.email.EmailFormatter; 11 | import com.yahoo.validatar.report.junit.JUnitFormatter; 12 | import joptsimple.OptionParser; 13 | import joptsimple.OptionSet; 14 | import lombok.AccessLevel; 15 | import lombok.Setter; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | import java.io.IOException; 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | /** 26 | * Manages the writing of test reports. 27 | */ 28 | @Slf4j 29 | public class FormatManager extends Pluggable implements Helpable { 30 | public static final String CUSTOM_FORMATTER = "custom-formatter"; 31 | public static final String CUSTOM_FORMATTER_DESCRIPTION = "Additional custom formatters to load."; 32 | 33 | public static final List> MANAGED_FORMATTERS = Arrays.asList(JUnitFormatter.class, EmailFormatter.class); 34 | public static final String REPORT_FORMAT = "report-format"; 35 | public static final String REPORT_ONLY_ON_FAILURE = "report-on-failure-only"; 36 | 37 | @Setter(AccessLevel.PACKAGE) 38 | private Map availableFormatters; 39 | @Setter(AccessLevel.PACKAGE) 40 | private List formattersToUse = new ArrayList<>(); 41 | private boolean onlyOnFailure = false; 42 | 43 | public static final String JUNIT = "junit"; 44 | // Leaving it here for now. If new formatters that require more complex options are needed, 45 | // it can be moved to inside the respective formatters. 46 | private static final OptionParser PARSER = new OptionParser() { 47 | { 48 | accepts(REPORT_FORMAT, "Which report formats to use.") 49 | .withRequiredArg() 50 | .describedAs("Report formats") 51 | .defaultsTo(JUNIT); 52 | accepts(REPORT_ONLY_ON_FAILURE, "Should the reporter be only run on failure.") 53 | .withRequiredArg() 54 | .describedAs("Report on failure") 55 | .ofType(Boolean.class) 56 | .defaultsTo(false); 57 | 58 | allowsUnrecognizedOptions(); 59 | } 60 | }; 61 | 62 | /** 63 | * Setups the format engine using the input parameters. 64 | * 65 | * @param arguments An array of parameters of the form [--param1 value1 --param2 value2...] 66 | */ 67 | @SuppressWarnings("unchecked") 68 | public FormatManager(String[] arguments) { 69 | super(MANAGED_FORMATTERS, CUSTOM_FORMATTER, CUSTOM_FORMATTER_DESCRIPTION); 70 | 71 | availableFormatters = new HashMap<>(); 72 | for (Formatter formatter : getPlugins(arguments)) { 73 | availableFormatters.put(formatter.getName(), formatter); 74 | log.info("Setup formatter {}", formatter.getName()); 75 | } 76 | 77 | OptionSet parser = PARSER.parse(arguments); 78 | onlyOnFailure = (Boolean) parser.valueOf(REPORT_ONLY_ON_FAILURE); 79 | List names = (List) parser.valuesOf(REPORT_FORMAT); 80 | names.forEach(name -> setupFormatter(name, arguments)); 81 | } 82 | 83 | /** 84 | * For testing purposes only. Pick the formatter to use. 85 | * 86 | * @param name The name of the formatter to setup 87 | */ 88 | void setupFormatter(String name, String[] arguments) { 89 | Formatter formatterToUse = availableFormatters.get(name); 90 | 91 | if (formatterToUse == null) { 92 | printHelp(); 93 | log.error("Could not find the formatter {}", formatterToUse); 94 | throw new NullPointerException("Could not find the formatter to use"); 95 | } 96 | 97 | if (!formatterToUse.setup(arguments)) { 98 | formatterToUse.printHelp(); 99 | log.error("Could not initialize the formatter {}", formatterToUse); 100 | throw new RuntimeException("Could not initialize the requested formatter"); 101 | } 102 | formattersToUse.add(formatterToUse); 103 | } 104 | 105 | /** 106 | * Write out a list of TestSuites unless this was configured to write out only on failures. 107 | * 108 | * @param testSuites List of test suites. 109 | * @throws java.io.IOException if any. 110 | */ 111 | public void writeReports(List testSuites) throws IOException { 112 | // Do nothing if we wanted to write out only on failures and we had no failures. 113 | if (onlyOnFailure && testSuites.stream().noneMatch(TestSuite::hasFailures)) { 114 | log.warn("Reports should be generated only on failure. Skipping reports since there were no failures."); 115 | return; 116 | } 117 | for (Formatter formatter : formattersToUse) { 118 | log.info("Writing report using {}...", formatter.getName()); 119 | formatter.writeReport(testSuites); 120 | } 121 | } 122 | 123 | @Override 124 | public void printHelp() { 125 | Helpable.printHelp("Reporting options", PARSER); 126 | availableFormatters.values().forEach(Formatter::printHelp); 127 | Helpable.printHelp("Advanced Reporting Options", getPluginOptionsParser()); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/report/FormatManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.report; 6 | 7 | import com.yahoo.validatar.common.Query; 8 | import com.yahoo.validatar.common.TestSuite; 9 | import org.testng.Assert; 10 | import org.testng.annotations.Test; 11 | 12 | import java.io.FileNotFoundException; 13 | import java.io.IOException; 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | import static com.yahoo.validatar.OutputCaptor.runWithoutOutput; 19 | 20 | public class FormatManagerTest { 21 | // Used for tests 22 | private class MockFormatter implements Formatter { 23 | public boolean wroteReport = false; 24 | 25 | @Override 26 | public boolean setup(String[] arguments) { 27 | return true; 28 | } 29 | 30 | @Override 31 | public void printHelp() { 32 | } 33 | 34 | @Override 35 | public void writeReport(List testSuites) throws IOException { 36 | wroteReport = true; 37 | } 38 | 39 | @Override 40 | public String getName() { 41 | return "MockFormat"; 42 | } 43 | } 44 | 45 | public static class FailingFormatter implements Formatter { 46 | public FailingFormatter() { 47 | } 48 | 49 | @Override 50 | public boolean setup(String[] arguments) { 51 | return false; 52 | } 53 | 54 | @Override 55 | public void printHelp() { 56 | } 57 | 58 | @Override 59 | public void writeReport(List testSuites) throws IOException { 60 | } 61 | 62 | @Override 63 | public String getName() { 64 | return "FailingFormat"; 65 | } 66 | } 67 | 68 | @Test(expectedExceptions = {NullPointerException.class}) 69 | public void testConstructorAndFindFormatterExceptionNoFormatterFound() throws FileNotFoundException { 70 | String[] args = {"--report-format", "INVALID"}; 71 | runWithoutOutput(() -> new FormatManager(args)); 72 | } 73 | 74 | @Test(expectedExceptions = {RuntimeException.class}) 75 | public void testNullFormatter() { 76 | String[] args = {}; 77 | FormatManager manager = new FormatManager(args); 78 | FailingFormatter formatter = new FailingFormatter(); 79 | manager.setAvailableFormatters(Collections.singletonMap("fail", formatter)); 80 | runWithoutOutput(() -> manager.setupFormatter("fail", args)); 81 | } 82 | 83 | @Test 84 | public void testWriteReport() throws IOException { 85 | String[] args = {}; 86 | FormatManager manager = new FormatManager(args); 87 | MockFormatter formatter = new MockFormatter(); 88 | manager.setAvailableFormatters(Collections.singletonMap("MockFormat", formatter)); 89 | manager.setFormattersToUse(new ArrayList<>()); 90 | manager.setupFormatter("MockFormat", args); 91 | manager.writeReports(null); 92 | Assert.assertTrue(formatter.wroteReport); 93 | } 94 | 95 | @Test 96 | public void testWriteReportOnlyOnFailureForFailingQueriesAndTests() throws IOException { 97 | String[] args = {"--report-on-failure-only", "true"}; 98 | MockFormatter formatter = new MockFormatter(); 99 | FormatManager manager = new FormatManager(args); 100 | manager.setAvailableFormatters(Collections.singletonMap("MockFormat", formatter)); 101 | manager.setFormattersToUse(new ArrayList<>()); 102 | manager.setupFormatter("MockFormat", args); 103 | TestSuite suite = new TestSuite(); 104 | 105 | Assert.assertFalse(formatter.wroteReport); 106 | 107 | com.yahoo.validatar.common.Test failingTest = new com.yahoo.validatar.common.Test(); 108 | failingTest.setFailed(); 109 | Query failingQuery = new Query(); 110 | failingQuery.setFailed(); 111 | suite.queries = Collections.singletonList(failingQuery); 112 | suite.tests = Collections.singletonList(failingTest); 113 | manager.writeReports(Collections.singletonList(suite)); 114 | Assert.assertTrue(formatter.wroteReport); 115 | } 116 | 117 | @Test 118 | public void testWriteReportOnlyOnFailureForWarnOnlyTests() throws IOException { 119 | String[] args = {"--report-on-failure-only", "true"}; 120 | MockFormatter formatter = new MockFormatter(); 121 | FormatManager manager = new FormatManager(args); 122 | manager.setAvailableFormatters(Collections.singletonMap("MockFormat", formatter)); 123 | manager.setFormattersToUse(new ArrayList<>()); 124 | manager.setupFormatter("MockFormat", args); 125 | TestSuite suite = new TestSuite(); 126 | 127 | Assert.assertFalse(formatter.wroteReport); 128 | 129 | com.yahoo.validatar.common.Test warnOnlyTest = new com.yahoo.validatar.common.Test(); 130 | warnOnlyTest.warnOnly = true; 131 | warnOnlyTest.setFailed(); 132 | suite.queries = null; 133 | suite.tests = Collections.singletonList(warnOnlyTest); 134 | manager.writeReports(Collections.singletonList(suite)); 135 | Assert.assertTrue(formatter.wroteReport); 136 | } 137 | 138 | @Test 139 | public void testWriteReportOnlyOnFailureForPassingQueriesAndTests() throws IOException { 140 | String[] args = {"--report-on-failure-only", "true"}; 141 | MockFormatter formatter = new MockFormatter(); 142 | FormatManager manager = new FormatManager(args); 143 | manager.setAvailableFormatters(Collections.singletonMap("MockFormat", formatter)); 144 | manager.setupFormatter("MockFormat", args); 145 | TestSuite suite = new TestSuite(); 146 | 147 | Assert.assertFalse(formatter.wroteReport); 148 | 149 | Query passingQuery = new Query(); 150 | suite.queries = Collections.singletonList(passingQuery); 151 | suite.tests = null; 152 | manager.writeReports(Collections.singletonList(suite)); 153 | Assert.assertFalse(formatter.wroteReport); 154 | 155 | com.yahoo.validatar.common.Test passingTest = new com.yahoo.validatar.common.Test(); 156 | suite.tests = Collections.singletonList(passingTest); 157 | manager.writeReports(Collections.singletonList(suite)); 158 | Assert.assertFalse(formatter.wroteReport); 159 | 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/resources/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/AppTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar; 6 | 7 | import com.yahoo.validatar.execution.EngineManager; 8 | import com.yahoo.validatar.execution.hive.Apiary; 9 | import com.yahoo.validatar.parse.ParseManager; 10 | import com.yahoo.validatar.report.FormatManager; 11 | import joptsimple.OptionParser; 12 | import joptsimple.OptionSet; 13 | import org.testng.Assert; 14 | import org.testng.annotations.Test; 15 | 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.sql.DriverManager; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import static com.yahoo.validatar.OutputCaptor.redirectToDevNull; 23 | import static com.yahoo.validatar.OutputCaptor.redirectToStandard; 24 | 25 | public class AppTest { 26 | private class MemoryDB extends Apiary { 27 | private final OptionParser parser = new OptionParser() { 28 | { 29 | accepts("hive-driver", "Fully qualified package name to the hive driver.") 30 | .withRequiredArg() 31 | .describedAs("Hive driver"); 32 | accepts("hive-jdbc", "JDBC string to the HiveServer. Ex: 'jdbc:hive2://HIVE_SERVER:PORT/DATABASENAME' ") 33 | .withRequiredArg() 34 | .describedAs("Hive JDBC connector."); 35 | accepts("hive-username", "Hive server username.") 36 | .withRequiredArg() 37 | .describedAs("Hive server username.") 38 | .defaultsTo("anon"); 39 | accepts("hive-password", "Hive server password.") 40 | .withRequiredArg() 41 | .describedAs("Hive server password.") 42 | .defaultsTo("anon"); 43 | allowsUnrecognizedOptions(); 44 | } 45 | }; 46 | 47 | @Override 48 | public boolean setup(String[] arguments) { 49 | try { 50 | OptionSet options = parser.parse(arguments); 51 | String driver = (String) options.valueOf("hive-driver"); 52 | Class.forName(driver); 53 | String jdbcConnector = (String) options.valueOf("hive-jdbc"); 54 | connection = DriverManager.getConnection(jdbcConnector, "", ""); 55 | } catch (Exception e) { 56 | throw new RuntimeException(e); 57 | } 58 | return true; 59 | } 60 | } 61 | 62 | private class CustomEngineManager extends EngineManager { 63 | public CustomEngineManager(String[] arguments) { 64 | super(new String[0]); 65 | this.arguments = arguments; 66 | this.engines = new HashMap<>(); 67 | MemoryDB db = new MemoryDB(); 68 | db.setup(arguments); 69 | engines.put(Apiary.ENGINE_NAME, new WorkingEngine(db)); 70 | } 71 | } 72 | 73 | @Test 74 | public void testFailRunTests() throws Exception { 75 | String[] args = {"--report-file", "target/AppTest-testFailRunTests.xml", 76 | "--hive-driver", "org.h2.Driver", 77 | "--hive-jdbc", "jdbc:h2:mem:"}; 78 | Map parameterMap = new HashMap<>(); 79 | File emptyTest = new File("src/test/resources/pig-tests/sample.yaml"); 80 | ParseManager parseManager = new ParseManager(args); 81 | EngineManager engineManager = new CustomEngineManager(args); 82 | FormatManager formatManager = new FormatManager(args); 83 | 84 | App.run(emptyTest, parameterMap, parseManager, engineManager, formatManager); 85 | Assert.assertFalse(new File("target/AppTest-testFailRunTests.xml").exists()); 86 | } 87 | 88 | @Test(expectedExceptions = {RuntimeException.class}) 89 | public void testParameterMissingToken() throws IOException { 90 | String[] args = {"--test-suite", "tests.yaml", "--parameter", "DATE:20140807"}; 91 | OptionSet options = App.parse(args); 92 | App.splitParameters(options, "parameter"); 93 | } 94 | 95 | @Test 96 | public void testParameterParsingFailure() throws IOException { 97 | String[] args = {"--parameter", "DATE:20140807"}; 98 | Assert.assertNull(new App().parse(args)); 99 | } 100 | 101 | @Test 102 | public void testSimpleParameterParse() throws IOException { 103 | // Fake CLI args 104 | String[] args = {"--test-suite", "tests.yaml", 105 | "--parameter", "DATE=2014071800", 106 | "--parameter", "NAME=ALPHA"}; 107 | 108 | // Parse CLI args 109 | Map paramMap; 110 | OptionSet options = App.parse(args); 111 | paramMap = App.splitParameters(options, "parameter"); 112 | 113 | // Check parse 114 | File testFile = (File) options.valueOf("test-suite"); 115 | Assert.assertEquals(testFile.getName(), "tests.yaml"); 116 | 117 | Assert.assertEquals(paramMap.get("DATE"), "2014071800"); 118 | Assert.assertEquals(paramMap.get("NAME"), "ALPHA"); 119 | } 120 | 121 | @Test 122 | public void testRunTests() throws Exception { 123 | String[] args = {"--report-file", "target/AppTest-testRunTests.xml", 124 | "--hive-driver", "org.h2.Driver", 125 | "--hive-jdbc", "jdbc:h2:mem:"}; 126 | Map parameterMap = new HashMap<>(); 127 | File emptyTest = new File("src/test/resources/sample-tests/empty-test.yaml"); 128 | ParseManager parseManager = new ParseManager(args); 129 | EngineManager engineManager = new CustomEngineManager(args); 130 | FormatManager formatManager = new FormatManager(args); 131 | 132 | App.run(emptyTest, parameterMap, parseManager, engineManager, formatManager); 133 | Assert.assertTrue(new File("target/AppTest-testRunTests.xml").exists()); 134 | } 135 | 136 | @Test 137 | public void testRun() throws IOException { 138 | redirectToDevNull(); 139 | 140 | App.run(new String[0]); 141 | 142 | String[] abbreviated = {"--h", "--test-suite", "src/test/resources/sample-tests"}; 143 | App.run(abbreviated); 144 | Assert.assertFalse(new File("target/AppTest-testMainHelpPrinting.xml").exists()); 145 | 146 | String[] nonAbbreviated = {"--help", "--test-suite", "src/test/resources/sample-tests"}; 147 | App.run(nonAbbreviated); 148 | Assert.assertFalse(new File("target/AppTest-testMainHelpPrinting.xml").exists()); 149 | 150 | String[] args = {"--report-file", "target/AppTest-testMainHelpPrinting.xml", 151 | "--test-suite", "src/test/resources/sample-tests", 152 | "--hive-driver", "org.h2.Driver", 153 | "--hive-jdbc", "jdbc:h2:mem:"}; 154 | App.run(args); 155 | Assert.assertFalse(new File("target/AppTest-testMainHelpPrinting.xml").exists()); 156 | 157 | redirectToStandard(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/App.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar; 6 | 7 | import com.yahoo.validatar.assertion.Assertor; 8 | import com.yahoo.validatar.common.Helpable; 9 | import com.yahoo.validatar.common.Query; 10 | import com.yahoo.validatar.common.Result; 11 | import com.yahoo.validatar.common.Test; 12 | import com.yahoo.validatar.common.TestSuite; 13 | import com.yahoo.validatar.execution.EngineManager; 14 | import com.yahoo.validatar.parse.ParseManager; 15 | import com.yahoo.validatar.report.FormatManager; 16 | import joptsimple.OptionParser; 17 | import joptsimple.OptionSet; 18 | import lombok.extern.slf4j.Slf4j; 19 | 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.util.Collection; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Objects; 27 | import java.util.stream.Collectors; 28 | 29 | import static java.util.Arrays.asList; 30 | 31 | @Slf4j 32 | public class App { 33 | public static final String PARAMETER = "parameter"; 34 | public static final String PARAMETER_DELIMITER = "="; 35 | public static final String TEST_SUITE = "test-suite"; 36 | public static final String HELP = "help"; 37 | public static final String HELP_ABBREVIATED = "h"; 38 | 39 | /** 40 | * The CLI parser. 41 | */ 42 | public static final OptionParser PARSER = new OptionParser() { 43 | { 44 | accepts(PARAMETER, "Parameter to replace all '${VAR}' in the query string. Ex: --parameter DATE=2014-07-24") 45 | .withRequiredArg() 46 | .describedAs("Parameter"); 47 | accepts(TEST_SUITE, "File or folder that contains the test suite file(s).") 48 | .withRequiredArg() 49 | .required() 50 | .ofType(File.class) 51 | .describedAs("Test suite file/folder"); 52 | acceptsAll(asList(HELP_ABBREVIATED, HELP), "Shows help message."); 53 | allowsUnrecognizedOptions(); 54 | } 55 | }; 56 | 57 | /** 58 | * Split parameters for string replacement. 59 | * 60 | * @param options Option set 61 | * @param parameterName Option parameter to split 62 | * @return Map of parameters and replacement strings 63 | */ 64 | public static Map splitParameters(OptionSet options, String parameterName) { 65 | Map parameterMap = new HashMap<>(); 66 | for (String parameter : (List) options.valuesOf(parameterName)) { 67 | String[] tokens = parameter.split(PARAMETER_DELIMITER); 68 | if (tokens.length != 2) { 69 | throw new RuntimeException("Invalid parameter. It should be KEY=VALUE. Found " + parameter); 70 | } 71 | parameterMap.put(tokens[0], tokens[1]); 72 | } 73 | return parameterMap; 74 | } 75 | 76 | /** 77 | * Parse arguements with parser. 78 | * 79 | * @param args CLI args 80 | * @return Option set containing all settings 81 | */ 82 | public static OptionSet parse(String[] args) { 83 | try { 84 | return PARSER.parse(args); 85 | } catch (Exception e) { 86 | return null; 87 | } 88 | } 89 | 90 | /** 91 | * Run the testSuite and parameters with the given Parse, Engine and Format Managers. 92 | * 93 | * @param testSuite The {@link File} where the TestSuite(s) are. 94 | * @param parameters An optional {@link Map} of parameters to their values to expand. 95 | * @param parseManager A {@link ParseManager} to use. 96 | * @param engineManager A {@link EngineManager} to use. 97 | * @param formatManager A {@link FormatManager} to use. 98 | * @return A boolean denoting whether all {@link Query} or {@link Test} passed. 99 | * @throws IOException if any. 100 | */ 101 | public static boolean run(File testSuite, Map parameters, ParseManager parseManager, 102 | EngineManager engineManager, FormatManager formatManager) throws IOException { 103 | // Load the test suite file(s) 104 | log.info("Parsing test files..."); 105 | List suites = parseManager.load(testSuite); 106 | log.info("Expanding parameters..."); 107 | ParseManager.deParametrize(suites, parameters); 108 | 109 | // Get the non-null queries 110 | List queries = suites.stream().map(s -> s.queries).filter(Objects::nonNull) 111 | .flatMap(Collection::stream).filter(Objects::nonNull).collect(Collectors.toList()); 112 | 113 | // Run the queries 114 | log.info("Running queries..."); 115 | if (!engineManager.run(queries)) { 116 | log.error("Error running queries. Failing..."); 117 | return false; 118 | } 119 | 120 | // Get the non-null query results 121 | List data = queries.stream().map(Query::getResult).filter(Objects::nonNull).collect(Collectors.toList()); 122 | 123 | // Get the non-null tests 124 | List tests = suites.stream().map(s -> s.tests).filter(Objects::nonNull) 125 | .flatMap(Collection::stream).filter(Objects::nonNull) 126 | .collect(Collectors.toList()); 127 | 128 | // Run the tests 129 | log.info("Running tests..."); 130 | Assertor.assertAll(data, tests); 131 | 132 | // Write reports 133 | log.info("Writing reports..."); 134 | formatManager.writeReports(suites); 135 | log.info("Done!"); 136 | 137 | return tests.stream().allMatch(Test::passed) && queries.stream().noneMatch(Query::failed); 138 | } 139 | 140 | /** 141 | * Runs Validatar with the given args. 142 | * 143 | * @param args The String arguments to Validatar. 144 | * @return A boolean denoting whether all tests or queries passed. 145 | * @throws IOException if any. 146 | */ 147 | public static boolean run(String[] args) throws IOException { 148 | // Parse CLI args 149 | OptionSet options = parse(args); 150 | 151 | ParseManager parseManager = new ParseManager(args); 152 | EngineManager engineManager = new EngineManager(args); 153 | FormatManager formatManager = new FormatManager(args); 154 | 155 | // Check if user needs help 156 | if (options == null || options.has(HELP_ABBREVIATED) || options.has(HELP)) { 157 | Helpable.printHelp("Application options", PARSER); 158 | parseManager.printHelp(); 159 | engineManager.printHelp(); 160 | formatManager.printHelp(); 161 | return true; 162 | } 163 | Map parameterMap = splitParameters(options, PARAMETER); 164 | 165 | return run((File) options.valueOf(TEST_SUITE), parameterMap, parseManager, engineManager, formatManager); 166 | } 167 | 168 | /** 169 | * Main. 170 | * 171 | * @param args The input arguments. 172 | * @throws IOException if any. 173 | */ 174 | public static void main(String[] args) throws IOException { 175 | System.exit(run(args) ? 0 : 1); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Code-of-Conduct.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: code-of-conduct 4 | nav_exclude: true 5 | --- 6 | 7 | # Yahoo Open Source Code of Conduct 8 | 9 | ## Summary 10 | This Code of Conduct is our way to encourage good behavior and discourage bad behavior in our open source projects. We invite participation from many people to bring different perspectives to our projects. We will do our part to foster a welcoming and professional environment free of harassment. We expect participants to communicate professionally and thoughtfully during their involvement with this project. 11 | 12 | Participants may lose their good standing by engaging in misconduct. For example: insulting, threatening, or conveying unwelcome sexual content. We ask participants who observe conduct issues to report the incident directly to the project's Response Team at opensource-conduct@yahooinc.com. Yahoo will assign a respondent to address the issue. We may remove harassers from this project. 13 | 14 | This code does not replace the terms of service or acceptable use policies of the websites used to support this project. We acknowledge that participants may be subject to additional conduct terms based on their employment which may govern their online expressions. 15 | 16 | ## Details 17 | This Code of Conduct makes our expectations of participants in this community explicit. 18 | * We forbid harassment and abusive speech within this community. 19 | * We request participants to report misconduct to the project’s Response Team. 20 | * We urge participants to refrain from using discussion forums to play out a fight. 21 | 22 | ### Expected Behaviors 23 | We expect participants in this community to conduct themselves professionally. Since our primary mode of communication is text on an online forum (e.g. issues, pull requests, comments, emails, or chats) devoid of vocal tone, gestures, or other context that is often vital to understanding, it is important that participants are attentive to their interaction style. 24 | 25 | * **Assume positive intent.** We ask community members to assume positive intent on the part of other people’s communications. We may disagree on details, but we expect all suggestions to be supportive of the community goals. 26 | * **Respect participants.** We expect occasional disagreements. Open Source projects are learning experiences. Ask, explore, challenge, and then _respectfully_ state if you agree or disagree. If your idea is rejected, be more persuasive not bitter. 27 | * **Welcoming to new members.** New members bring new perspectives. Some ask questions that have been addressed before. _Kindly_ point to existing discussions. Everyone is new to every project once. 28 | * **Be kind to beginners.** Beginners use open source projects to get experience. They might not be talented coders yet, and projects should not accept poor quality code. But we were all beginners once, and we need to engage kindly. 29 | * **Consider your impact on others.** Your work will be used by others, and you depend on the work of others. We expect community members to be considerate and establish a balance their self-interest with communal interest. 30 | * **Use words carefully.** We may not understand intent when you say something ironic. Often, people will misinterpret sarcasm in online communications. We ask community members to communicate plainly. 31 | * **Leave with class.** When you wish to resign from participating in this project for any reason, you are free to fork the code and create a competitive project. Open Source explicitly allows this. Your exit should not be dramatic or bitter. 32 | 33 | ### Unacceptable Behaviors 34 | Participants remain in good standing when they do not engage in misconduct or harassment (some examples follow). We do not list all forms of harassment, nor imply some forms of harassment are not worthy of action. Any participant who *feels* harassed or *observes* harassment, should report the incident to the Response Team. 35 | * **Don't be a bigot.** Calling out project members by their identity or background in a negative or insulting manner. This includes, but is not limited to, slurs or insinuations related to protected or suspect classes e.g. race, color, citizenship, national origin, political belief, religion, sexual orientation, gender identity and expression, age, size, culture, ethnicity, genetic features, language, profession, national minority status, mental or physical ability. 36 | * **Don't insult.** Insulting remarks about a person’s lifestyle practices. 37 | * **Don't dox.** Revealing private information about other participants without explicit permission. 38 | * **Don't intimidate.** Threats of violence or intimidation of any project member. 39 | * **Don't creep.** Unwanted sexual attention or content unsuited for the subject of this project. 40 | * **Don't inflame.** We ask that victim of harassment not address their grievances in the public forum, as this often intensifies the problem. Report it, and let us address it off-line. 41 | * **Don't disrupt.** Sustained disruptions in a discussion. 42 | 43 | ### Reporting Issues 44 | If you experience or witness misconduct, or have any other concerns about the conduct of members of this project, please report it by contacting our Response Team at opensource-conduct@yahooinc.com who will handle your report with discretion. Your report should include: 45 | * Your preferred contact information. We cannot process anonymous reports. 46 | * Names (real or usernames) of those involved in the incident. 47 | * Your account of what occurred, and if the incident is ongoing. Please provide links to or transcripts of the publicly available records (e.g. a mailing list archive or a public IRC logger), so that we can review it. 48 | * Any additional information that may be helpful to achieve resolution. 49 | 50 | After filing a report, a representative will contact you directly to review the incident and ask additional questions. If a member of the Yahoo Response Team is named in an incident report, that member will be recused from handling your incident. If the complaint originates from a member of the Response Team, it will be addressed by a different member of the Response Team. We will consider reports to be confidential for the purpose of protecting victims of abuse. 51 | 52 | ### Scope 53 | Yahoo will assign a Response Team member with admin rights on the project and legal rights on the project copyright. The Response Team is empowered to restrict some privileges to the project as needed. Since this project is governed by an open source license, any participant may fork the code under the terms of the project license. The Response Team’s goal is to preserve the project if possible, and will restrict or remove participation from those who disrupt the project. 54 | 55 | This code does not replace the terms of service or acceptable use policies that are provided by the websites used to support this community. Nor does this code apply to communications or actions that take place outside of the context of this community. Many participants in this project are also subject to codes of conduct based on their employment. This code is a social-contract that informs participants of our social expectations. It is not a terms of service or legal contract. 56 | 57 | ## License and Acknowledgment. 58 | This text is shared under the [CC-BY-4.0 license](https://creativecommons.org/licenses/by/4.0/). This code is based on a study conducted by the [TODO Group](https://todogroup.org/) of many codes used in the open source community. If you have feedback about this code, contact our Response Team at the address listed above. 59 | 60 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/parse/ParseManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.parse; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | import com.yahoo.validatar.common.Metadata; 9 | import com.yahoo.validatar.common.Pluggable; 10 | import com.yahoo.validatar.common.Query; 11 | import com.yahoo.validatar.common.Test; 12 | import com.yahoo.validatar.common.TestSuite; 13 | import com.yahoo.validatar.parse.yaml.YAML; 14 | import lombok.extern.slf4j.Slf4j; 15 | 16 | import java.io.File; 17 | import java.io.FileInputStream; 18 | import java.util.Collection; 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Objects; 24 | import java.util.regex.Matcher; 25 | import java.util.regex.Pattern; 26 | import java.util.stream.Collectors; 27 | import java.util.stream.Stream; 28 | 29 | @Slf4j 30 | public class ParseManager extends Pluggable implements FileLoadable, Helpable { 31 | public static final String CUSTOM_PARSER = "custom-parser"; 32 | public static final String CUSTOM_PARSER_DESCRIPTION = "Additional custom parser to load."; 33 | 34 | /** 35 | * The Parser classes to manage. 36 | */ 37 | public static final List> MANAGED_PARSERS = Collections.singletonList(YAML.class); 38 | 39 | public static final Pattern REGEX = Pattern.compile("\\$\\{(.*?)\\}"); 40 | 41 | private final HashMap availableParsers; 42 | 43 | /** 44 | * Constructor. Default. 45 | * 46 | * @param arguments The String array of arguments to this constructor. 47 | */ 48 | public ParseManager(String[] arguments) { 49 | super(MANAGED_PARSERS, CUSTOM_PARSER, CUSTOM_PARSER_DESCRIPTION); 50 | availableParsers = new HashMap<>(); 51 | for (Parser parser : getPlugins(arguments)) { 52 | availableParsers.put(parser.getName(), parser); 53 | log.info("Setup parser {}", parser.getName()); 54 | } 55 | } 56 | 57 | @Override 58 | public List load(File path) { 59 | return getFiles(path).map(this::getTestSuite).filter(Objects::nonNull).collect(Collectors.toList()); 60 | } 61 | 62 | private Stream getFiles(File path) { 63 | if (path == null) { 64 | return Stream.empty(); 65 | } 66 | if (path.isFile()) { 67 | log.info("TestSuite parameter {} is a file. Loading...", path); 68 | return Stream.of(path); 69 | } 70 | log.info("TestSuite parameter {} is a folder. Loading all files inside...", path); 71 | File[] files = path.listFiles(); 72 | if (files == null) { 73 | log.warn("No files found in {}. Skipping...", path); 74 | return Stream.empty(); 75 | } 76 | return Stream.of(files).sorted(); 77 | } 78 | 79 | /** 80 | * Takes a List of non null TestSuite and a map and replaces all the variables in the query with the value 81 | * in the map, in place. 82 | * 83 | * @param suites A list of TestSuites containing parametrized queries. 84 | * @param parameterMap A non null map of parameters to their values. 85 | */ 86 | public static void deParametrize(List suites, Map parameterMap) { 87 | Objects.requireNonNull(suites); 88 | 89 | suites.stream().filter(Objects::nonNull).map(s -> s.tests) 90 | .flatMap(Collection::stream).filter(Objects::nonNull) 91 | .forEach(t -> deParametrize(t, parameterMap)); 92 | 93 | suites.stream().filter(Objects::nonNull).map(s -> s.queries) 94 | .flatMap(Collection::stream).filter(Objects::nonNull) 95 | .forEach(q -> deParametrize(q, parameterMap)); 96 | } 97 | 98 | /** 99 | * Takes a non null Query and replaces all the variables in the query 100 | * with the value in the map, in place. 101 | * 102 | * @param test A Test that could be parametrized. 103 | * @param parameterMap A map of parameters to their values. 104 | */ 105 | public static void deParametrize(Test test, Map parameterMap) { 106 | Objects.requireNonNull(test); 107 | Objects.requireNonNull(parameterMap); 108 | if (test.asserts == null) { 109 | return; 110 | } 111 | test.asserts = test.asserts.stream().map(assertString -> deParametrize(assertString, parameterMap)) 112 | .collect(Collectors.toList()); 113 | } 114 | 115 | /** 116 | * Takes a non null Query and replaces all the variables in the query 117 | * with the value in the map, in place. 118 | * 119 | * @param query A Query that could be parametrized. 120 | * @param parameterMap A map of parameters to their values. 121 | */ 122 | public static void deParametrize(Query query, Map parameterMap) { 123 | Objects.requireNonNull(query); 124 | Objects.requireNonNull(parameterMap); 125 | query.value = deParametrize(query.value, parameterMap); 126 | if (query.metadata == null) { 127 | return; 128 | } 129 | query.metadata = query.metadata.stream().map(m -> new Metadata(deParametrize(m.key, parameterMap), 130 | deParametrize(m.value, parameterMap))) 131 | .collect(Collectors.toList()); 132 | } 133 | 134 | /** 135 | * Takes a non null String and replaces all variables in it with the value of the 136 | * variable in the map. 137 | * 138 | * @param source The original string with parameters 139 | * @param parameterMap A map of parameters to the their values. 140 | * @return The new String with the replaced variables. 141 | */ 142 | public static String deParametrize(String source, Map parameterMap) { 143 | Matcher matcher = REGEX.matcher(source); 144 | StringBuffer replaced = new StringBuffer(); 145 | while (matcher.find()) { 146 | String parameterValue = parameterMap.get(matcher.group(1)); 147 | if (parameterValue != null) { 148 | matcher.appendReplacement(replaced, parameterValue); 149 | } 150 | } 151 | matcher.appendTail(replaced); 152 | return replaced.toString(); 153 | } 154 | 155 | /** 156 | * Takes a non null File and parses a TestSuite out of it. 157 | * 158 | * @param path A non null File object representing the file. 159 | * @return The parsed TestSuite from the file. Null if it cannot be parsed. 160 | */ 161 | protected TestSuite getTestSuite(File path) { 162 | Objects.requireNonNull(path); 163 | if (!path.isFile()) { 164 | log.error("Path {} is not a file.", path); 165 | return null; 166 | } 167 | Parser parser = availableParsers.get(getFileExtension(path.getName())); 168 | if (parser == null) { 169 | log.error("Unable to parse {}. File extension does not match any known parsers. Skipping...", path); 170 | return null; 171 | } 172 | try { 173 | return parser.parse(new FileInputStream(path)); 174 | } catch (Exception e) { 175 | log.error("Could not parse the TestSuite", e); 176 | return null; 177 | } 178 | } 179 | 180 | private String getFileExtension(String fileName) { 181 | int index = fileName.lastIndexOf('.'); 182 | return (index > 0) ? fileName.substring(index + 1) : null; 183 | } 184 | 185 | @Override 186 | public void printHelp() { 187 | Helpable.printHelp("Advanced Parsing Options", getPluginOptionsParser()); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/report/email/EmailFormatter.java: -------------------------------------------------------------------------------- 1 | package com.yahoo.validatar.report.email; 2 | 3 | import com.yahoo.validatar.common.Helpable; 4 | import com.yahoo.validatar.common.TestSuite; 5 | import com.yahoo.validatar.report.Formatter; 6 | import joptsimple.OptionException; 7 | import joptsimple.OptionParser; 8 | import joptsimple.OptionSet; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.jtwig.JtwigModel; 11 | import org.jtwig.JtwigTemplate; 12 | import org.simplejavamail.email.Email; 13 | import org.simplejavamail.email.EmailBuilder; 14 | import org.simplejavamail.mailer.Mailer; 15 | import org.simplejavamail.mailer.config.ServerConfig; 16 | import org.simplejavamail.mailer.config.TransportStrategy; 17 | 18 | import java.io.IOException; 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.List; 22 | 23 | import static java.util.Arrays.asList; 24 | 25 | /** 26 | * This formatter renders the report as an HTML report and mails it to specified recipients. 27 | */ 28 | @Slf4j 29 | public class EmailFormatter implements Formatter { 30 | public static final String EMAIL_FORMATTER = "email"; 31 | 32 | public static final String EMAIL_RECIPIENT = "email-recipient"; 33 | public static final String EMAIL_RECIPIENTS = "email-recipients"; 34 | public static final String EMAIL_SENDER_NAME = "email-sender-name"; 35 | public static final String EMAIL_SUBJECT_PREFIX = "email-subject-prefix"; 36 | public static final String EMAIL_FROM = "email-from"; 37 | public static final String EMAIL_REPLY_TO = "email-reply-to"; 38 | public static final String EMAIL_SMTP_HOST = "email-smtp-host"; 39 | public static final String EMAIL_SMTP_PORT = "email-smtp-port"; 40 | public static final String EMAIL_SMTP_STRATEGY = "email-smtp-strategy"; 41 | 42 | private static final String DEFAULT_STRATEGY = "SMTP_TLS"; 43 | 44 | private static final OptionParser PARSER = new OptionParser() { 45 | { 46 | acceptsAll(asList(EMAIL_RECIPIENT, EMAIL_RECIPIENTS), "Comma-separated or multi-option emails to send reports") 47 | .withRequiredArg() 48 | .required() 49 | .describedAs("Report recipients' emails") 50 | .withValuesSeparatedBy(COMMA); 51 | accepts(EMAIL_SENDER_NAME, "Name of sender displayed to report recipients") 52 | .withRequiredArg() 53 | .defaultsTo("Validatar"); 54 | accepts(EMAIL_SUBJECT_PREFIX, "Prefix for the subject of the email") 55 | .withRequiredArg() 56 | .defaultsTo("[VALIDATAR] Test Status - "); 57 | accepts(EMAIL_FROM, "Email shown to recipients as 'from'") 58 | .withRequiredArg() 59 | .required(); 60 | accepts(EMAIL_REPLY_TO, "Email to which replies will be sent") 61 | .withRequiredArg() 62 | .required(); 63 | accepts(EMAIL_SMTP_HOST, "Email SMTP host name") 64 | .withRequiredArg() 65 | .required(); 66 | accepts(EMAIL_SMTP_PORT, "Email SMTP port") 67 | .withRequiredArg() 68 | .required(); 69 | accepts(EMAIL_SMTP_STRATEGY, "Email SMTP transport strategy - SMTP_PLAIN, SMTP_TLS, SMTP_SSL") 70 | .withRequiredArg() 71 | .defaultsTo(DEFAULT_STRATEGY); 72 | allowsUnrecognizedOptions(); 73 | } 74 | }; 75 | 76 | private static final String TWIG_TEMPLATE = "templates/email.twig"; 77 | private static final String TWIG_ERROR_PARAM = "error"; 78 | private static final String TWIG_TEST_LIST_PARAM = "testList"; 79 | 80 | private static final String COMMA = ","; 81 | 82 | private static final String SUBJECT_FAILURE_SUFFIX = "Errored"; 83 | private static final String SUBJECT_SUCCESS_SUFFIX = "Passed"; 84 | 85 | private static final String X_PRIORITY = "X-Priority"; 86 | private static final int PRIORITY = 2; 87 | 88 | private List recipientEmails; 89 | private String senderName; 90 | private String subjectPrefix; 91 | private String fromEmail; 92 | private String replyTo; 93 | private String smtpHost; 94 | private int smtpPort; 95 | private TransportStrategy strategy; 96 | 97 | @Override 98 | @SuppressWarnings("unchecked") 99 | public boolean setup(String[] arguments) { 100 | OptionSet options; 101 | try { 102 | options = PARSER.parse(arguments); 103 | } catch (OptionException e) { 104 | log.error("EmailFormatter is missing required arguments", e); 105 | return false; 106 | } 107 | senderName = (String) options.valueOf(EMAIL_SENDER_NAME); 108 | subjectPrefix = (String) options.valueOf(EMAIL_SUBJECT_PREFIX); 109 | fromEmail = (String) options.valueOf(EMAIL_FROM); 110 | replyTo = (String) options.valueOf(EMAIL_REPLY_TO); 111 | smtpHost = (String) options.valueOf(EMAIL_SMTP_HOST); 112 | smtpPort = Integer.parseInt((String) options.valueOf(EMAIL_SMTP_PORT)); 113 | recipientEmails = (List) options.valuesOf(EMAIL_RECIPIENTS); 114 | strategy = TransportStrategy.valueOf((String) options.valueOf(EMAIL_SMTP_STRATEGY)); 115 | return true; 116 | } 117 | 118 | /** 119 | * {@inheritDoc} 120 | *

121 | * Render the report HTML using Jtwig and send the result to the recipient emails. 122 | */ 123 | @Override 124 | public void writeReport(List testSuites) throws IOException { 125 | if (testSuites == null) { 126 | testSuites = Collections.emptyList(); 127 | } 128 | log.info("Sending report email for {} test suites", testSuites.size()); 129 | List testList = new ArrayList<>(testSuites.size()); 130 | boolean hasError = false; 131 | for (TestSuite testSuite : testSuites) { 132 | TestSuiteModel testSuiteModel = new TestSuiteModel(testSuite); 133 | hasError = hasError || !testSuiteModel.allPassed(); 134 | testList.add(testSuiteModel); 135 | } 136 | JtwigTemplate template = JtwigTemplate.classpathTemplate(TWIG_TEMPLATE); 137 | JtwigModel model = JtwigModel.newModel() 138 | .with(TWIG_ERROR_PARAM, hasError) 139 | .with(TWIG_TEST_LIST_PARAM, testList); 140 | String reportHtml = template.render(model); 141 | EmailBuilder emailBuilder = 142 | new EmailBuilder().from(senderName, fromEmail) 143 | .replyTo(senderName, replyTo) 144 | .subject(subjectPrefix + (hasError ? SUBJECT_FAILURE_SUFFIX : SUBJECT_SUCCESS_SUFFIX)) 145 | .addHeader(X_PRIORITY, PRIORITY) .textHTML(reportHtml); 146 | for (String recipientEmail : recipientEmails) { 147 | log.info("Emailing {}", recipientEmail); 148 | emailBuilder.to(recipientEmail); 149 | } 150 | Email reportEmail = emailBuilder.build(); 151 | ServerConfig mailServerConfig = new ServerConfig(smtpHost, smtpPort); 152 | Mailer reportMailer = new Mailer(mailServerConfig, strategy); 153 | sendEmail(reportMailer, reportEmail); 154 | log.info("Finished sending report to recipients"); 155 | } 156 | 157 | /** 158 | * Method uses the provided mailer to send the email report. 159 | * 160 | * @param mailer mailer to use 161 | * @param email report email 162 | */ 163 | protected void sendEmail(Mailer mailer, Email email) { 164 | mailer.sendMail(email); 165 | } 166 | 167 | @Override 168 | public String getName() { 169 | return EMAIL_FORMATTER; 170 | } 171 | 172 | @Override 173 | public void printHelp() { 174 | Helpable.printHelp("Email report options", PARSER); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/execution/fixed/DSV.java: -------------------------------------------------------------------------------- 1 | package com.yahoo.validatar.execution.fixed; 2 | 3 | import com.yahoo.validatar.common.Helpable; 4 | import com.yahoo.validatar.common.Query; 5 | import com.yahoo.validatar.common.Result; 6 | import com.yahoo.validatar.common.TypeSystem; 7 | import com.yahoo.validatar.common.TypeSystem.Type; 8 | import com.yahoo.validatar.common.TypedObject; 9 | import com.yahoo.validatar.execution.Engine; 10 | import joptsimple.OptionParser; 11 | import joptsimple.OptionSet; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.csv.CSVFormat; 14 | import org.apache.commons.csv.CSVParser; 15 | import org.apache.commons.csv.CSVRecord; 16 | import org.apache.commons.lang3.StringEscapeUtils; 17 | 18 | import java.io.ByteArrayInputStream; 19 | import java.io.File; 20 | import java.io.FileInputStream; 21 | import java.io.InputStream; 22 | import java.io.InputStreamReader; 23 | import java.io.Reader; 24 | import java.nio.charset.StandardCharsets; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | @Slf4j 29 | public class DSV implements Engine { 30 | public static final String ENGINE_NAME = "csv"; 31 | 32 | public static final String CSV_DELIMITER = "csv-delimiter"; 33 | public static final String METADATA_DELIMITER_KEY = "delimiter"; 34 | 35 | public static final String DEFAULT_TYPE = Type.STRING.name(); 36 | public static final String DEFAULT_DELIMITER = ","; 37 | 38 | private String defaultDelimiter; 39 | 40 | private final OptionParser parser = new OptionParser() { 41 | { 42 | accepts(CSV_DELIMITER, "The delimiter to use while parsing fields within a record. Defaults to ',' or CSV") 43 | .withRequiredArg() 44 | .describedAs("The field delimiter") 45 | .defaultsTo(DEFAULT_DELIMITER); 46 | allowsUnrecognizedOptions(); 47 | } 48 | }; 49 | 50 | @Override 51 | public boolean setup(String[] arguments) { 52 | OptionSet options = parser.parse(arguments); 53 | defaultDelimiter = (String) options.valueOf(CSV_DELIMITER); 54 | return true; 55 | } 56 | 57 | @Override 58 | public void execute(Query query) { 59 | String queryName = query.name; 60 | String queryValue = query.value.trim(); 61 | log.info("Running {}", queryName); 62 | 63 | Map metadata = query.getMetadata(); 64 | 65 | String delimiter = Query.getKey(metadata, METADATA_DELIMITER_KEY).orElse(defaultDelimiter); 66 | char character = delimiter.charAt(0); 67 | log.info("Using delimiter as a character: {}", character); 68 | 69 | boolean isFile = isPath(queryValue); 70 | 71 | log.info("Running or loading data from \n{}", queryValue); 72 | 73 | try (InputStream stream = isFile ? new FileInputStream(queryValue) : new ByteArrayInputStream(getBytes(queryValue)); 74 | Reader reader = new InputStreamReader(stream)) { 75 | 76 | CSVFormat format = CSVFormat.RFC4180.withDelimiter(character).withFirstRecordAsHeader().withIgnoreSurroundingSpaces(); 77 | CSVParser parser = new CSVParser(reader, format); 78 | 79 | Map headerIndices = parser.getHeaderMap(); 80 | Map typeMap = getTypeMapping(headerIndices, metadata); 81 | Result result = query.createResults(); 82 | parser.iterator().forEachRemaining(r -> addRow(r, result, typeMap)); 83 | } catch (Exception e) { 84 | log.error("Error while parsing data for {} with {}", queryName, queryValue); 85 | log.error("Error", e); 86 | query.setFailure(e.toString()); 87 | } 88 | } 89 | 90 | private static void addRow(CSVRecord record, Result result, Map header) { 91 | if (!record.isConsistent()) { 92 | log.warn("Record does not have the same number of fields as the header mapping. Skipping record: {}", record); 93 | return; 94 | } 95 | for (Map.Entry field : header.entrySet()) { 96 | String fieldName = field.getKey(); 97 | TypedObject fieldValue = getTyped(field.getValue(), record.get(fieldName)); 98 | result.addColumnRow(fieldName, fieldValue); 99 | } 100 | } 101 | 102 | private static TypedObject getTyped(Type type, String field) { 103 | // Can't be null 104 | if (type != Type.STRING && field.isEmpty()) { 105 | log.warn("Found an empty value for a non-string field of type {}. Nulled it. Asserts that use this may fail.", type); 106 | return null; 107 | } 108 | return TypeSystem.cast(type, new TypedObject(field, Type.STRING)); 109 | } 110 | 111 | /** 112 | * Gets a mapping of the {@link Type} for the columns in the data. 113 | * 114 | * @param headerIndices The {@link Map} of header field names to their positions. 115 | * @param metadata The metadata of the query viewed as a {@link Map}. 116 | * @return The {@link Map} of column names to their types. 117 | */ 118 | static Map getTypeMapping(Map headerIndices, Map metadata) { 119 | if (headerIndices == null) { 120 | log.error("No header row found. The first row in your data needs to be a header row."); 121 | throw new RuntimeException("Header row not found for data. First row in data needs to be a header"); 122 | } 123 | 124 | Map typeMap = new HashMap<>(); 125 | for (String column : headerIndices.keySet()) { 126 | String typeMapping = Query.getKey(metadata, column).orElse(DEFAULT_TYPE); 127 | try { 128 | Type type = Type.valueOf(typeMapping); 129 | typeMap.put(column, type); 130 | } catch (IllegalArgumentException iae) { 131 | log.error("Unable to find type {}. Using STRING instead. Valid values are {}", typeMapping, Type.values()); 132 | typeMap.put(column, Type.STRING); 133 | } 134 | } 135 | return typeMap; 136 | } 137 | 138 | private static byte[] getBytes(String string) { 139 | String unescaped = StringEscapeUtils.unescapeJava(string); 140 | return unescaped.getBytes(StandardCharsets.UTF_8); 141 | } 142 | 143 | private static boolean isPath(String string) { 144 | File file = new File(string); 145 | return file.exists() && file.isFile(); 146 | } 147 | 148 | @Override 149 | public String getName() { 150 | return ENGINE_NAME; 151 | } 152 | 153 | @Override 154 | public void printHelp() { 155 | Helpable.printHelp("CSV Engine options", parser); 156 | System.out.println("This Engine lets you load delimited text data from files or to specify it directly as a query."); 157 | System.out.println("It follows the RFC 4180 CSV specification: https://tools.ietf.org/html/rfc4180\n"); 158 | System.out.println("Your data MUST contain a header row naming your columns."); 159 | System.out.println("The types of all fields will be inferred as STRINGS. However, you can provide mappings "); 160 | System.out.println("for each column name by adding entries to the metadata section of the query, where"); 161 | System.out.println("the key is the name of your column and the value is the type of the column."); 162 | System.out.println("The values can be BOOLEAN, STRING, LONG, DECIMAL, DOUBLE, and TIMESTAMP."); 163 | System.out.println("DECIMAL is used for really large numbers that cannot fit inside a long (2^63). TIMESTAMP is"); 164 | System.out.println("used to interpret a whole number as a timestamp field - millis from epoch. Use to load dates."); 165 | System.out.println("This engine primarily exists to let you easily load expected data in as a dataset. You can"); 166 | System.out.println("then use the data by joining it with some other data and performing asserts on the joined"); 167 | System.out.println("dataset."); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/execution/fixed/DSVTest.java: -------------------------------------------------------------------------------- 1 | package com.yahoo.validatar.execution.fixed; 2 | 3 | import com.yahoo.validatar.common.Column; 4 | import com.yahoo.validatar.common.Metadata; 5 | import com.yahoo.validatar.common.Query; 6 | import com.yahoo.validatar.common.Result; 7 | import com.yahoo.validatar.common.TypeSystem.Type; 8 | import org.testng.Assert; 9 | import org.testng.annotations.BeforeMethod; 10 | import org.testng.annotations.Test; 11 | 12 | import java.io.IOException; 13 | import java.math.BigDecimal; 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | import static com.yahoo.validatar.OutputCaptor.runWithoutOutput; 19 | import static com.yahoo.validatar.TestHelpers.asColumn; 20 | import static com.yahoo.validatar.TestHelpers.getQueryFrom; 21 | import static com.yahoo.validatar.TestHelpers.isEqual; 22 | import static java.util.Collections.singletonMap; 23 | 24 | public class DSVTest { 25 | private final String[] defaults = {"--csv-delimiter", ","}; 26 | private DSV dsv; 27 | 28 | @BeforeMethod 29 | public void setup() { 30 | dsv = new DSV(); 31 | dsv.setup(defaults); 32 | } 33 | 34 | @Test 35 | public void testDefaults() { 36 | Assert.assertTrue(dsv.setup(new String[0])); 37 | Assert.assertEquals(dsv.getName(), DSV.ENGINE_NAME); 38 | runWithoutOutput(dsv::printHelp); 39 | } 40 | 41 | @Test 42 | public void testEmptyQuery() { 43 | Query query = new Query(); 44 | query.value = ""; 45 | dsv.execute(query); 46 | 47 | Assert.assertFalse(query.failed()); 48 | Assert.assertEquals(query.getResult().numberOfRows(), 0); 49 | Assert.assertEquals(query.getResult().getColumns().size(), 0); 50 | } 51 | 52 | @Test 53 | public void testMissingValuesForStrings() { 54 | Query query = new Query(); 55 | query.value = "foo,bar\n0,1\n,3"; 56 | dsv.execute(query); 57 | 58 | Assert.assertFalse(query.failed()); 59 | Column foo = query.getResult().getColumn("foo"); 60 | Column bar = query.getResult().getColumn("bar"); 61 | Assert.assertEquals(foo.get(0).data, "0"); 62 | Assert.assertEquals(foo.get(1).data, ""); 63 | Assert.assertEquals(bar.get(0).data, "1"); 64 | Assert.assertEquals(bar.get(1).data, "3"); 65 | } 66 | 67 | @Test 68 | public void testMissingValuesForNonStrings() { 69 | Query query = new Query(); 70 | 71 | query.metadata = new ArrayList<>(); 72 | Metadata metadata = new Metadata(); 73 | metadata.key = "foo"; 74 | metadata.value = Type.LONG.name(); 75 | query.metadata.add(metadata); 76 | 77 | query.value = "foo,bar\n0,1\n,3"; 78 | 79 | dsv.execute(query); 80 | 81 | Assert.assertFalse(query.failed()); 82 | Column foo = query.getResult().getColumn("foo"); 83 | Column bar = query.getResult().getColumn("bar"); 84 | Assert.assertEquals(foo.get(0).data, 0L); 85 | Assert.assertNull(foo.get(1)); 86 | Assert.assertEquals(bar.get(0).data, "1"); 87 | Assert.assertEquals(bar.get(1).data, "3"); 88 | } 89 | 90 | @Test 91 | public void testDuplicateHeader() { 92 | Query query = new Query(); 93 | query.value = ","; 94 | dsv.execute(query); 95 | 96 | Assert.assertTrue(query.failed()); 97 | Assert.assertTrue(query.getMessages().get(0).contains("header contains a duplicate")); 98 | } 99 | 100 | @Test 101 | public void testBadPath() { 102 | Query query = new Query(); 103 | query.value = "src/test/resources/csv-tests"; 104 | dsv.execute(query); 105 | 106 | Assert.assertFalse(query.failed()); 107 | Assert.assertEquals(query.getResult().numberOfRows(), 0); 108 | Assert.assertEquals(query.getResult().getColumns().size(), 0); 109 | } 110 | 111 | @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = ".*Header row not found.*") 112 | public void testNoHeader() { 113 | DSV.getTypeMapping(null, null); 114 | } 115 | 116 | @Test 117 | public void testTypeMapping() { 118 | Map headers = new HashMap<>(); 119 | headers.put("foo", 0); 120 | headers.put("bar", 1); 121 | headers.put("baz", 2); 122 | headers.put("qux", 3); 123 | 124 | Map mapping = new HashMap<>(); 125 | mapping.put("foo", "DECIMAL"); 126 | mapping.put("baz", "TIMESTAMP"); 127 | 128 | Map result = DSV.getTypeMapping(headers, mapping); 129 | Assert.assertEquals(result.get("foo"), Type.DECIMAL); 130 | Assert.assertEquals(result.get("bar"), Type.STRING); 131 | Assert.assertEquals(result.get("baz"), Type.TIMESTAMP); 132 | Assert.assertEquals(result.get("qux"), Type.STRING); 133 | } 134 | 135 | @Test 136 | public void testForcedDefaultTypeMapping() { 137 | Map result = DSV.getTypeMapping(singletonMap("foo", 0), singletonMap("foo", "GARBAGE")); 138 | Assert.assertEquals(result.get("foo"), Type.STRING); 139 | } 140 | 141 | @Test 142 | public void testStringLoading() throws IOException { 143 | Query query = getQueryFrom("csv-tests/sample.yaml", "StringTest"); 144 | dsv.execute(query); 145 | 146 | Result actual = query.getResult(); 147 | Result expected = new Result("StringTest"); 148 | expected.addColumn("A", asColumn(Type.STRING, "foo", "baz", "foo")); 149 | expected.addColumn("B", asColumn(Type.DOUBLE, 234.3, 9.0, 42.0)); 150 | expected.addColumn("C", asColumn(Type.STRING, "bar", "qux", "norf")); 151 | 152 | Assert.assertTrue(isEqual(actual, expected)); 153 | } 154 | 155 | @Test 156 | public void testFileLoading() throws IOException { 157 | Query query = getQueryFrom("csv-tests/sample.yaml", "FileLoadingTest"); 158 | dsv.execute(query); 159 | 160 | Result actual = query.getResult(); 161 | Result expected = new Result("FileLoadingTest"); 162 | // We should have skipped a row that contained more fields than the header 163 | // We should have nulled out a field that was missing a value but was not a STRING. 164 | // We should have used an empty STRING for a missing field that was of type STRING 165 | expected.addColumn("fieldA", asColumn(Type.STRING, "foo", "baz", "qux", "bar")); 166 | expected.addColumn("fieldB", asColumn(Type.LONG, 15L, 42L, 0L, null)); 167 | expected.addColumn("fieldC", asColumn(Type.DECIMAL, new BigDecimal("34.2"), new BigDecimal("4.2"), 168 | new BigDecimal("8.4"), new BigDecimal("0.2"))); 169 | expected.addColumn("fieldD", asColumn(Type.STRING, "bar", "bar", "norf", "")); 170 | 171 | Assert.assertTrue(isEqual(actual, expected)); 172 | } 173 | 174 | @Test 175 | public void testCustomDelimiterASCII() throws IOException { 176 | Query query = getQueryFrom("csv-tests/sample.yaml", "CustomDelimASCII"); 177 | dsv.execute(query); 178 | 179 | Result actual = query.getResult(); 180 | Result expected = new Result("CustomDelimASCII"); 181 | expected.addColumn("A", asColumn(Type.STRING, "foo", "baz", "foo")); 182 | expected.addColumn("B", asColumn(Type.STRING, "234.3", "9", "42")); 183 | expected.addColumn("C", asColumn(Type.STRING, "bar", "qux", "norf")); 184 | 185 | Assert.assertTrue(isEqual(actual, expected)); 186 | } 187 | 188 | @Test 189 | public void testCustomDelimiterUnicode() throws IOException { 190 | Query query = getQueryFrom("csv-tests/sample.yaml", "CustomDelimUnicode"); 191 | dsv.execute(query); 192 | 193 | Result actual = query.getResult(); 194 | Result expected = new Result("CustomDelimUnicode"); 195 | expected.addColumn("A", asColumn(Type.STRING, "foo", "baz", "foo")); 196 | expected.addColumn("B", asColumn(Type.STRING, "234.3", "9", "42")); 197 | expected.addColumn("C", asColumn(Type.STRING, "bar", "qux", "norf")); 198 | 199 | Assert.assertTrue(isEqual(actual, expected)); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/execution/EngineManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.execution; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | import com.yahoo.validatar.common.Pluggable; 9 | import com.yahoo.validatar.common.Query; 10 | import com.yahoo.validatar.execution.fixed.DSV; 11 | import com.yahoo.validatar.execution.hive.Apiary; 12 | import com.yahoo.validatar.execution.pig.Sty; 13 | import com.yahoo.validatar.execution.rest.JSON; 14 | import joptsimple.OptionParser; 15 | import joptsimple.OptionSet; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | import java.util.Arrays; 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.TreeMap; 24 | import java.util.concurrent.ForkJoinPool; 25 | import java.util.stream.Collectors; 26 | 27 | /** 28 | * Manages the creation and execution of execution engines. 29 | */ 30 | @Slf4j 31 | public class EngineManager extends Pluggable implements Helpable { 32 | public static final String CUSTOM_ENGINE = "custom-engine"; 33 | public static final String CUSTOM_ENGINE_DESCRIPTION = "Additional custom engine to load."; 34 | public static final String QUERY_PARALLEL_ENABLE = "query-parallel-enable"; 35 | public static final String QUERY_PARALLEL_MAX = "query-parallel-max"; 36 | private static final int QUERY_PARALLEL_MIN = 1; 37 | 38 | protected boolean queryParallelEnable; 39 | protected int queryParallelMax; 40 | 41 | private static final OptionParser PARSER = new OptionParser() { 42 | { 43 | accepts(QUERY_PARALLEL_ENABLE, "Whether or not queries should run in parallel.") 44 | .withRequiredArg() 45 | .describedAs("Query parallelism option") 46 | .ofType(Boolean.class) 47 | .defaultsTo(false); 48 | accepts(QUERY_PARALLEL_MAX, "The max number of queries that will run concurrently. If non-positive or " + 49 | "unspecified, all queries will run at once.") 50 | .withRequiredArg() 51 | .describedAs("Max query parallelism") 52 | .ofType(Integer.class) 53 | .defaultsTo(0); 54 | allowsUnrecognizedOptions(); 55 | } 56 | }; 57 | 58 | /** 59 | * The Engine classes to manage. 60 | */ 61 | public static final List> MANAGED_ENGINES = Arrays.asList(Apiary.class, Sty.class, JSON.class, DSV.class); 62 | 63 | /** 64 | * Stores the CLI arguments. 65 | */ 66 | protected String[] arguments; 67 | 68 | /** 69 | * Stores engine names to engine references. 70 | */ 71 | protected Map engines; 72 | 73 | /** 74 | * A simple wrapper to mark an engine as started. 75 | */ 76 | protected class WorkingEngine { 77 | public boolean isStarted = false; 78 | private Engine engine = null; 79 | 80 | /** 81 | * Constructor. 82 | * 83 | * @param engine The engine to wrap. 84 | */ 85 | public WorkingEngine(Engine engine) { 86 | this.engine = engine; 87 | } 88 | 89 | /** 90 | * Getter. 91 | * 92 | * @return The wrapped Engine. 93 | */ 94 | public Engine getEngine() { 95 | return this.engine; 96 | } 97 | } 98 | 99 | /** 100 | * Store arguments and create the engine map. 101 | * 102 | * @param arguments CLI arguments. 103 | */ 104 | public EngineManager(String[] arguments) { 105 | super(MANAGED_ENGINES, CUSTOM_ENGINE, CUSTOM_ENGINE_DESCRIPTION); 106 | 107 | this.arguments = arguments; 108 | 109 | // Create the engines map, engine name -> engine 110 | engines = new HashMap<>(); 111 | for (Engine engine : getPlugins(arguments)) { 112 | engines.put(engine.getName(), new WorkingEngine(engine)); 113 | log.info("Added engine {} to list of engines.", engine.getName()); 114 | } 115 | 116 | OptionSet parser = PARSER.parse(arguments); 117 | queryParallelEnable = (Boolean) parser.valueOf(QUERY_PARALLEL_ENABLE); 118 | queryParallelMax = (Integer) parser.valueOf(QUERY_PARALLEL_MAX); 119 | } 120 | 121 | /** 122 | * For testing purposes to inject engines. Will always override existing engines. 123 | * 124 | * @param engines A list of engines to use as the engines to work with. 125 | */ 126 | void setEngines(List engines) { 127 | List all = engines == null ? Collections.emptyList() : engines; 128 | this.engines = all.stream().collect(Collectors.toMap(Engine::getName, WorkingEngine::new)); 129 | } 130 | 131 | /** 132 | * For a list of queries, start corresponding engines. 133 | * 134 | * @param queries Queries to check for engine support. 135 | * @return true iff the required engines were loaded. 136 | */ 137 | protected boolean startEngines(List queries) { 138 | List all = queries == null ? Collections.emptyList() : queries; 139 | // Queries -> engine name Set -> start engine -> verify all started 140 | return all.stream().map(q -> q.engine).distinct().allMatch(this::startEngine); 141 | } 142 | 143 | private boolean startEngine(String engine) { 144 | WorkingEngine working = engines.get(engine); 145 | if (working == null) { 146 | log.error("Engine {} not loaded but required by query.", engine); 147 | return false; 148 | } 149 | // Already started? 150 | if (working.isStarted) { 151 | return true; 152 | } 153 | working.isStarted = working.getEngine().setup(arguments); 154 | if (!working.isStarted) { 155 | log.error("Required engine {} could not be setup.", engine); 156 | working.getEngine().printHelp(); 157 | return false; 158 | } 159 | return true; 160 | } 161 | 162 | @Override 163 | public void printHelp() { 164 | Helpable.printHelp("Engine Options", PARSER); 165 | engines.values().stream().map(WorkingEngine::getEngine).forEach(Engine::printHelp); 166 | Helpable.printHelp("Advanced Engine Options", getPluginOptionsParser()); 167 | } 168 | 169 | private void run(Query query) { 170 | try { 171 | engines.get(query.engine).getEngine().execute(query); 172 | } catch (Exception e) { 173 | query.setFailure(e.toString()); 174 | } 175 | } 176 | 177 | /** 178 | * Run a query and store the results in the query object. 179 | * 180 | * @param queries Queries to execute and store. 181 | * @return true iff the required engines were loaded and the queries were able to run. 182 | */ 183 | public boolean run(List queries) { 184 | if (queries.isEmpty()) { 185 | return true; 186 | } 187 | if (!startEngines(queries)) { 188 | return false; 189 | } 190 | // Run each query. 191 | if (!queryParallelEnable) { 192 | queries.forEach(this::run); 193 | } else { 194 | // Split queries into groups by priority where lowers value correspond to higher priority and run first 195 | Map> queryGroups = queries.stream().collect(Collectors.groupingBy(Query::getPriority, TreeMap::new, Collectors.toList())); 196 | int maxGroupSize = queryGroups.values().stream().mapToInt(List::size).max().getAsInt(); 197 | int poolSize = Math.max(queryParallelMax > 0 ? queryParallelMax : maxGroupSize, QUERY_PARALLEL_MIN); 198 | log.info("Creating a ForkJoinPool with size {}", poolSize); 199 | ForkJoinPool forkJoinPool = new ForkJoinPool(poolSize); 200 | queryGroups.values().forEach(queryGroup -> { 201 | try { 202 | forkJoinPool.submit(() -> queryGroup.parallelStream().forEach(this::run)).get(); 203 | } catch (Exception e) { 204 | log.error("Caught exception", e); 205 | } 206 | }); 207 | forkJoinPool.shutdown(); 208 | } 209 | return true; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/execution/pig/Sty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.execution.pig; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | import com.yahoo.validatar.common.Query; 9 | import com.yahoo.validatar.common.Result; 10 | import com.yahoo.validatar.common.TypeSystem; 11 | import com.yahoo.validatar.common.TypedObject; 12 | import com.yahoo.validatar.execution.Engine; 13 | import joptsimple.OptionParser; 14 | import joptsimple.OptionSet; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.apache.pig.PigServer; 17 | import org.apache.pig.backend.executionengine.ExecException; 18 | import org.apache.pig.data.DataType; 19 | import org.apache.pig.data.Tuple; 20 | import org.apache.pig.impl.logicalLayer.schema.Schema; 21 | 22 | import java.io.ByteArrayInputStream; 23 | import java.io.IOException; 24 | import java.sql.Timestamp; 25 | import java.util.Collections; 26 | import java.util.Iterator; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Properties; 30 | import java.util.stream.Collectors; 31 | 32 | @Slf4j 33 | public class Sty implements Engine { 34 | public static final String PIG_EXEC_TYPE = "pig-exec-type"; 35 | public static final String PIG_OUTPUT_ALIAS = "pig-output-alias"; 36 | public static final String PIG_SETTING = "pig-setting"; 37 | 38 | /** Engine name. */ 39 | public static final String ENGINE_NAME = "pig"; 40 | 41 | public static final String DEFAULT_EXEC_TYPE = "mr"; 42 | public static final String DEFAULT_OUTPUT_ALIAS = "validatar_results"; 43 | public static final String SETTING_DELIMITER = "="; 44 | 45 | public static final String METADATA_EXEC_TYPE_KEY = "exec-type"; 46 | public static final String METADATA_ALIAS_KEY = "output-alias"; 47 | 48 | private String defaultExecType; 49 | private String defaultOutputAlias; 50 | private Properties properties; 51 | 52 | private class FieldDetail { 53 | public final String alias; 54 | public final byte type; 55 | 56 | public FieldDetail(String alias, byte type) { 57 | this.alias = alias; 58 | this.type = type; 59 | } 60 | } 61 | 62 | private final OptionParser parser = new OptionParser() { 63 | { 64 | accepts(PIG_EXEC_TYPE, "The exec-type for Pig to use (the -x argument used when running Pig. Ex: local, mr, tez)") 65 | .withRequiredArg() 66 | .describedAs("Pig execution type") 67 | .defaultsTo(DEFAULT_EXEC_TYPE); 68 | accepts(PIG_OUTPUT_ALIAS, "The default name of the alias where the result is. This should contain the data that will be collected") 69 | .withRequiredArg() 70 | .describedAs("Pig default output alias") 71 | .defaultsTo(DEFAULT_OUTPUT_ALIAS); 72 | accepts(PIG_SETTING, "Settings and their values. The -D params that would have been sent to Pig. Ex: 'mapreduce.job.acl-view-job=*'") 73 | .withRequiredArg() 74 | .describedAs("Pig generic settings to use."); 75 | allowsUnrecognizedOptions(); 76 | } 77 | }; 78 | 79 | @Override 80 | public String getName() { 81 | return ENGINE_NAME; 82 | } 83 | 84 | @Override 85 | public boolean setup(String[] arguments) { 86 | OptionSet options = parser.parse(arguments); 87 | defaultExecType = (String) options.valueOf(PIG_EXEC_TYPE); 88 | defaultOutputAlias = (String) options.valueOf(PIG_OUTPUT_ALIAS); 89 | properties = getProperties(options); 90 | // We will boot up a PigServer per query, so nothing else to do... 91 | return true; 92 | } 93 | 94 | @Override 95 | public void printHelp() { 96 | Helpable.printHelp("Pig engine options", parser); 97 | } 98 | 99 | @Override 100 | public void execute(Query query) { 101 | String queryName = query.name; 102 | String queryValue = query.value; 103 | Map queryMetadata = query.getMetadata(); 104 | String execType = Query.getKey(queryMetadata, METADATA_EXEC_TYPE_KEY).orElse(defaultExecType); 105 | String alias = Query.getKey(queryMetadata, METADATA_ALIAS_KEY).orElse(defaultOutputAlias); 106 | log.info("Running {} for alias {}: {}", queryName, alias, queryValue); 107 | try { 108 | PigServer server = getPigServer(execType); 109 | server.registerScript(new ByteArrayInputStream(queryValue.getBytes())); 110 | Iterator queryResults = server.openIterator(alias); 111 | Result result = query.createResults(); 112 | // dumpSchema will also, unfortunately, print the schema to stdout. 113 | List metadata = getFieldDetails(server.dumpSchema(alias)); 114 | populateColumns(metadata, result); 115 | while (queryResults.hasNext()) { 116 | populateRow(queryResults.next(), metadata, result); 117 | } 118 | server.shutdown(); 119 | } catch (IOException ioe) { 120 | log.error("Problem with Pig query: {}\n{}", queryValue, ioe); 121 | query.setFailure(ioe.toString()); 122 | } catch (Exception e) { 123 | log.error("Error occurred while processing Pig query: {}\n{}", queryValue, e); 124 | query.setFailure(e.toString()); 125 | } 126 | } 127 | 128 | private void populateColumns(List metadata, Result result) throws IOException { 129 | if (metadata.isEmpty()) { 130 | throw new IOException("No metadata of columns found for Pig query"); 131 | } 132 | metadata.forEach(m -> result.addColumn(m.alias)); 133 | } 134 | 135 | private void populateRow(Tuple row, List metadata, Result result) throws ExecException { 136 | if (row == null) { 137 | log.info("Skipping null row in results..."); 138 | return; 139 | } 140 | for (int i = 0; i < metadata.size(); ++i) { 141 | FieldDetail column = metadata.get(i); 142 | TypedObject value = getTypedObject(row.get(i), column); 143 | log.info("Column: {}\tType: {}\tValue: {}", column.alias, column.type, (value == null ? "null" : value.data)); 144 | result.addColumnRow(column.alias, value); 145 | } 146 | } 147 | 148 | private TypedObject getTypedObject(Object data, FieldDetail detail) throws ExecException { 149 | if (data == null) { 150 | return null; 151 | } 152 | byte type = detail.type; 153 | switch (type) { 154 | case DataType.BOOLEAN: 155 | return TypeSystem.asTypedObject(DataType.toBoolean(data, type)); 156 | case DataType.INTEGER: 157 | case DataType.LONG: 158 | return TypeSystem.asTypedObject(DataType.toLong(data, type)); 159 | case DataType.FLOAT: 160 | case DataType.DOUBLE: 161 | return TypeSystem.asTypedObject(DataType.toDouble(data, type)); 162 | case DataType.DATETIME: 163 | return TypeSystem.asTypedObject(new Timestamp(DataType.toDateTime(data, type).getMillis())); 164 | case DataType.BYTE: 165 | case DataType.BYTEARRAY: 166 | case DataType.CHARARRAY: 167 | return TypeSystem.asTypedObject(DataType.toString(data, type)); 168 | case DataType.BIGINTEGER: 169 | case DataType.BIGDECIMAL: 170 | return TypeSystem.asTypedObject(DataType.toBigDecimal(data, type)); 171 | default: 172 | //TUPLE, BAG, MAP, INTERNALMAP, GENERIC_WRITABLECOMPARABLE, ERROR, UNKNOWN, NULL and anything else 173 | return null; 174 | } 175 | } 176 | 177 | private List getFieldDetails(Schema schema) { 178 | if (schema == null) { 179 | return Collections.emptyList(); 180 | } 181 | return schema.getFields().stream().map(f -> new FieldDetail(f.alias, f.type)).collect(Collectors.toList()); 182 | } 183 | 184 | private Properties getProperties(OptionSet options) { 185 | List settings = (List) options.valuesOf(PIG_SETTING); 186 | Properties properties = new Properties(); 187 | for (String setting : settings) { 188 | String[] tokens = setting.split(SETTING_DELIMITER); 189 | if (tokens.length != 2) { 190 | log.error("Ignoring unknown Pig setting provided: {}", setting); 191 | continue; 192 | } 193 | properties.put(tokens[0], tokens[1]); 194 | } 195 | return properties; 196 | } 197 | 198 | PigServer getPigServer(String execType) throws IOException { 199 | return new PigServer(execType, properties); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Operations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import java.util.Objects; 8 | import java.util.function.BinaryOperator; 9 | import java.util.function.UnaryOperator; 10 | 11 | import static com.yahoo.validatar.common.TypeSystem.asTypedObject; 12 | import static com.yahoo.validatar.common.TypeSystem.compare; 13 | 14 | /** 15 | * This defines the various operations we will support and provides default implementations for all of them. A particular 16 | * type specific implementation can implement this to provide its own specific logic. 17 | */ 18 | public interface Operations { 19 | /** 20 | * These are the unary operations we will support. 21 | */ 22 | enum UnaryOperation { 23 | CAST, NOT 24 | } 25 | 26 | /** 27 | * These are the binary operations we will support. 28 | */ 29 | enum BinaryOperation { 30 | ADD, SUBTRACT, MULTIPLY, DIVIDE, MODULUS, EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_EQUAL, LESS_EQUAL, OR, AND 31 | } 32 | 33 | /** 34 | * Adds two TypedObjects. 35 | * 36 | * @param first The first object. 37 | * @param second The second object. 38 | * @return The result object. 39 | */ 40 | default TypedObject add(TypedObject first, TypedObject second) { 41 | return null; 42 | } 43 | 44 | /** 45 | * Subtracts two TypedObjects. 46 | * 47 | * @param first The first object. 48 | * @param second The second object. 49 | * @return The result object. 50 | */ 51 | default TypedObject subtract(TypedObject first, TypedObject second) { 52 | return null; 53 | } 54 | 55 | /** 56 | * Multiplies two TypedObjects. 57 | * 58 | * @param first The first object. 59 | * @param second The second object. 60 | * @return The result object. 61 | */ 62 | default TypedObject multiply(TypedObject first, TypedObject second) { 63 | return null; 64 | } 65 | 66 | /** 67 | * Divides two TypedObjects. 68 | * 69 | * @param first The first object. 70 | * @param second The second object. 71 | * @return The result object. 72 | */ 73 | default TypedObject divide(TypedObject first, TypedObject second) { 74 | return null; 75 | } 76 | 77 | /** 78 | * Finds the integer remainder after division two TypedObjects. 79 | * 80 | * @param first The first object. 81 | * @param second The second object. 82 | * @return The result object. 83 | */ 84 | default TypedObject modulus(TypedObject first, TypedObject second) { 85 | return null; 86 | } 87 | 88 | /** 89 | * Checks to see if the two {@link TypedObject} are equal. 90 | * 91 | * @param first The first object. 92 | * @param second The second object. 93 | * @return The result TypedObject of type {@link TypeSystem.Type#BOOLEAN}. 94 | */ 95 | default TypedObject equal(TypedObject first, TypedObject second) { 96 | return asTypedObject(compare(first, second) == 0); 97 | } 98 | 99 | /** 100 | * Checks to see if the two {@link TypedObject} are not equal. 101 | * 102 | * @param first The first object. 103 | * @param second The second object. 104 | * @return The result TypedObject of type {@link TypeSystem.Type#BOOLEAN}. 105 | */ 106 | default TypedObject notEqual(TypedObject first, TypedObject second) { 107 | return asTypedObject(compare(first, second) != 0); 108 | } 109 | 110 | /** 111 | * Checks to see if the first {@link TypedObject} is greater than the second. 112 | * 113 | * @param first The first object. 114 | * @param second The second object. 115 | * @return The result TypedObject of type {@link TypeSystem.Type#BOOLEAN}. 116 | */ 117 | default TypedObject greater(TypedObject first, TypedObject second) { 118 | return asTypedObject(compare(first, second) > 0); 119 | } 120 | 121 | /** 122 | * Checks to see if the first {@link TypedObject} is less than the second. 123 | * 124 | * @param first The first object. 125 | * @param second The second object. 126 | * @return The result TypedObject of type {@link TypeSystem.Type#BOOLEAN}. 127 | */ 128 | default TypedObject less(TypedObject first, TypedObject second) { 129 | return asTypedObject(compare(first, second) < 0); 130 | } 131 | 132 | /** 133 | * Checks to see if the first {@link TypedObject} is greater than or equal to the second. 134 | * 135 | * @param first The first object. 136 | * @param second The second object. 137 | * @return The result TypedObject of type {@link TypeSystem.Type#BOOLEAN}. 138 | */ 139 | default TypedObject greaterEqual(TypedObject first, TypedObject second) { 140 | return asTypedObject(compare(first, second) >= 0); 141 | } 142 | 143 | /** 144 | * Checks to see if the first {@link TypedObject} is less than or equal to the second. 145 | * 146 | * @param first The first object. 147 | * @param second The second object. 148 | * @return The result TypedObject of type {@link TypeSystem.Type#BOOLEAN}. 149 | */ 150 | default TypedObject lessEqual(TypedObject first, TypedObject second) { 151 | return asTypedObject(compare(first, second) <= 0); 152 | } 153 | 154 | /** 155 | * Logical ors two TypedObjects. 156 | * 157 | * @param first The first object. 158 | * @param second The second object. 159 | * @return The result object. 160 | */ 161 | default TypedObject or(TypedObject first, TypedObject second) { 162 | return null; 163 | } 164 | 165 | /** 166 | * Logical ands two TypedObjects. 167 | * 168 | * @param first The first object. 169 | * @param second The second object. 170 | * @return The result object. 171 | */ 172 | default TypedObject and(TypedObject first, TypedObject second) { 173 | return null; 174 | } 175 | 176 | /** 177 | * Logical negates a TypedObject. 178 | * 179 | * @param object The object. 180 | * @return The result object. 181 | */ 182 | default TypedObject not(TypedObject object) { 183 | return null; 184 | } 185 | 186 | /** 187 | * Casts a TypedObject into its given type. 188 | * 189 | * @param object The object. 190 | * @return The result object. 191 | */ 192 | default TypedObject cast(TypedObject object) { 193 | return null; 194 | } 195 | 196 | /** 197 | * Given a BinaryOperation, finds the operator for it. Null if it cannot. 198 | * 199 | * @param operation The operation 200 | * @return The result binary operator that can be applied. 201 | */ 202 | default BinaryOperator dispatch(BinaryOperation operation) { 203 | // Can assign to a return value and return it, getting rid of the unreachable default... 204 | Objects.requireNonNull(operation); 205 | BinaryOperator operator = null; 206 | switch (operation) { 207 | case ADD: 208 | operator = this::add; 209 | break; 210 | case SUBTRACT: 211 | operator = this::subtract; 212 | break; 213 | case MULTIPLY: 214 | operator = this::multiply; 215 | break; 216 | case DIVIDE: 217 | operator = this::divide; 218 | break; 219 | case MODULUS: 220 | operator = this::modulus; 221 | break; 222 | case EQUAL: 223 | operator = this::equal; 224 | break; 225 | case NOT_EQUAL: 226 | operator = this::notEqual; 227 | break; 228 | case GREATER: 229 | operator = this::greater; 230 | break; 231 | case LESS: 232 | operator = this::less; 233 | break; 234 | case GREATER_EQUAL: 235 | operator = this::greaterEqual; 236 | break; 237 | case LESS_EQUAL: 238 | operator = this::lessEqual; 239 | break; 240 | case OR: 241 | operator = this::or; 242 | break; 243 | case AND: 244 | operator = this::and; 245 | break; 246 | } 247 | return operator; 248 | } 249 | 250 | /** 251 | * Given a UnaryOperation, finds the operator for it. Null if it cannot. 252 | * 253 | * @param operation The operation. 254 | * @return The result unary operator that can be applied. 255 | */ 256 | default UnaryOperator dispatch(UnaryOperation operation) { 257 | Objects.requireNonNull(operation); 258 | UnaryOperator operator = null; 259 | switch (operation) { 260 | case NOT: 261 | operator = this::not; 262 | break; 263 | case CAST: 264 | operator = this::cast; 265 | break; 266 | } 267 | return operator; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/execution/hive/Apiary.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.execution.hive; 6 | 7 | import com.yahoo.validatar.common.Helpable; 8 | import com.yahoo.validatar.common.Query; 9 | import com.yahoo.validatar.common.Result; 10 | import com.yahoo.validatar.common.TypeSystem; 11 | import com.yahoo.validatar.common.TypedObject; 12 | import com.yahoo.validatar.execution.Engine; 13 | import joptsimple.OptionParser; 14 | import joptsimple.OptionSet; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | import java.sql.Connection; 18 | import java.sql.DriverManager; 19 | import java.sql.ResultSet; 20 | import java.sql.ResultSetMetaData; 21 | import java.sql.SQLException; 22 | import java.sql.Statement; 23 | import java.sql.Types; 24 | import java.util.List; 25 | 26 | @Slf4j 27 | public class Apiary implements Engine { 28 | public static final String HIVE_JDBC = "hive-jdbc"; 29 | public static final String HIVE_DRIVER = "hive-driver"; 30 | public static final String HIVE_USERNAME = "hive-username"; 31 | public static final String HIVE_PASSWORD = "hive-password"; 32 | public static final String HIVE_SETTING = "hive-setting"; 33 | 34 | public static final String ENGINE_NAME = "hive"; 35 | 36 | public static final String DRIVER_NAME = "org.apache.hive.jdbc.HiveDriver"; 37 | public static final String SETTING_PREFIX = "set "; 38 | 39 | protected Connection connection; 40 | protected OptionSet options; 41 | 42 | private final OptionParser parser = new OptionParser() { 43 | { 44 | accepts(HIVE_JDBC, "JDBC string to the HiveServer2 with an optional database. " + 45 | "If the database is provided, the queries must NOT have one. " + 46 | "Ex: 'jdbc:hive2://HIVE_SERVER:PORT/[DATABASE_FOR_ALL_QUERIES]' ") 47 | .withRequiredArg() 48 | .required() 49 | .describedAs("Hive JDBC connector"); 50 | accepts(HIVE_DRIVER, "Fully qualified package name to the hive driver.") 51 | .withRequiredArg() 52 | .describedAs("Hive driver") 53 | .defaultsTo(DRIVER_NAME); 54 | accepts(HIVE_USERNAME, "Hive server username.") 55 | .withRequiredArg() 56 | .describedAs("Hive server username") 57 | .defaultsTo("anon"); 58 | accepts(HIVE_PASSWORD, "Hive server password.") 59 | .withRequiredArg() 60 | .describedAs("Hive server password") 61 | .defaultsTo("anon"); 62 | accepts(HIVE_SETTING, "Settings and their values. Ex: 'hive.execution.engine=mr'") 63 | .withRequiredArg() 64 | .describedAs("Hive generic settings to use."); 65 | allowsUnrecognizedOptions(); 66 | } 67 | }; 68 | 69 | @Override 70 | public boolean setup(String[] arguments) { 71 | options = parser.parse(arguments); 72 | try { 73 | connection = setupConnection(); 74 | } catch (ClassNotFoundException | SQLException e) { 75 | log.error("Could not set up the Hive engine", e); 76 | return false; 77 | } 78 | return true; 79 | } 80 | 81 | @Override 82 | public void printHelp() { 83 | Helpable.printHelp("Hive engine options", parser); 84 | } 85 | 86 | @Override 87 | public void execute(Query query) { 88 | String queryName = query.name; 89 | String queryValue = query.value; 90 | log.info("Running {}: {}", queryName, queryValue); 91 | try (Statement statement = connection.createStatement()) { 92 | setHiveSettings(statement); 93 | ResultSet result = statement.executeQuery(queryValue); 94 | ResultSetMetaData metadata = result.getMetaData(); 95 | int columns = metadata.getColumnCount(); 96 | 97 | Result queryResult = query.createResults(); 98 | 99 | addHeader(metadata, columns, queryResult); 100 | while (result.next()) { 101 | addRow(result, metadata, columns, queryResult); 102 | } 103 | result.close(); 104 | } catch (SQLException e) { 105 | log.error("SQL problem with Hive query: {}\n{}\n{}", queryName, queryValue, e); 106 | query.setFailure(e.getMessage()); 107 | } 108 | } 109 | 110 | private void addHeader(ResultSetMetaData metadata, int columns, Result queryResult) throws SQLException { 111 | for (int i = 1; i < columns + 1; i++) { 112 | String name = metadata.getColumnName(i); 113 | queryResult.addColumn(name); 114 | } 115 | } 116 | 117 | private void addRow(ResultSet result, ResultSetMetaData metadata, int columns, Result storage) throws SQLException { 118 | for (int i = 1; i < columns + 1; i++) { 119 | // The name and type getting is being done per row. We should fix it even though Hive gets it only once. 120 | String name = metadata.getColumnName(i); 121 | int type = metadata.getColumnType(i); 122 | TypedObject value = getAsTypedObject(result, i, type); 123 | storage.addColumnRow(name, value); 124 | log.info("Column: {}\tType: {}\tValue: {}", name, type, (value == null ? "null" : value.data)); 125 | } 126 | } 127 | 128 | @Override 129 | public String getName() { 130 | return ENGINE_NAME; 131 | } 132 | 133 | /** 134 | * Takes a value and its type and returns it as the appropriate TypedObject. 135 | * 136 | * @param results The ResultSet that has a confirmed value for reading by its iterator. 137 | * @param index The index of the column in the results to get. 138 | * @param type The java.sql.TypesSQL type of the value. 139 | * @return A non-null TypedObject representation of the value or null if the result was null. 140 | * @throws java.sql.SQLException if any. 141 | */ 142 | TypedObject getAsTypedObject(ResultSet results, int index, int type) throws SQLException { 143 | if (results.getObject(index) == null || results.wasNull()) { 144 | return null; 145 | } 146 | 147 | TypedObject toReturn; 148 | switch (type) { 149 | case (Types.DATE): 150 | case (Types.CHAR): 151 | case (Types.VARCHAR): 152 | toReturn = TypeSystem.asTypedObject(results.getString(index)); 153 | break; 154 | case (Types.FLOAT): 155 | case (Types.DOUBLE): 156 | toReturn = TypeSystem.asTypedObject(results.getDouble(index)); 157 | break; 158 | case (Types.BOOLEAN): 159 | toReturn = TypeSystem.asTypedObject(results.getBoolean(index)); 160 | break; 161 | case (Types.TINYINT): 162 | case (Types.SMALLINT): 163 | case (Types.INTEGER): 164 | case (Types.BIGINT): 165 | toReturn = TypeSystem.asTypedObject(results.getLong(index)); 166 | break; 167 | case (Types.DECIMAL): 168 | toReturn = TypeSystem.asTypedObject(results.getBigDecimal(index)); 169 | break; 170 | case (Types.TIMESTAMP): 171 | toReturn = TypeSystem.asTypedObject(results.getTimestamp(index)); 172 | break; 173 | case (Types.NULL): 174 | toReturn = null; 175 | break; 176 | default: 177 | throw new UnsupportedOperationException("Unknown SQL type encountered from Hive: " + type); 178 | } 179 | return toReturn; 180 | } 181 | 182 | /** 183 | * Sets up the connection using JDBC. 184 | * 185 | * @return The created {@link java.sql.Statement} object. 186 | * @throws java.lang.ClassNotFoundException if any. 187 | * @throws java.sql.SQLException if any. 188 | */ 189 | Connection setupConnection() throws ClassNotFoundException, SQLException { 190 | // Load the JDBC driver 191 | String driver = (String) options.valueOf(HIVE_DRIVER); 192 | log.info("Loading JDBC driver: {}", driver); 193 | Class.forName(driver); 194 | 195 | // Get the JDBC connector 196 | String jdbcConnector = (String) options.valueOf(HIVE_JDBC); 197 | 198 | log.info("Connecting to: {}", jdbcConnector); 199 | String username = (String) options.valueOf(HIVE_USERNAME); 200 | String password = (String) options.valueOf(HIVE_PASSWORD); 201 | 202 | // Start the connection 203 | return DriverManager.getConnection(jdbcConnector, username, password); 204 | } 205 | 206 | /** 207 | * Applies any settings if provided. 208 | * 209 | * @param statement A {@link java.sql.Statement} to execute the setting updates to. 210 | * @throws java.sql.SQLException if any. 211 | */ 212 | void setHiveSettings(Statement statement) throws SQLException { 213 | for (String setting : (List) options.valuesOf(HIVE_SETTING)) { 214 | log.info("Applying setting {}", setting); 215 | statement.executeUpdate(SETTING_PREFIX + setting); 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/test/java/com/yahoo/validatar/report/email/EmailFormatterTest.java: -------------------------------------------------------------------------------- 1 | package com.yahoo.validatar.report.email; 2 | 3 | import com.yahoo.validatar.OutputCaptor; 4 | import com.yahoo.validatar.common.Query; 5 | import com.yahoo.validatar.common.TestSuite; 6 | import org.simplejavamail.email.Email; 7 | import org.simplejavamail.mailer.Mailer; 8 | import org.simplejavamail.mailer.config.TransportStrategy; 9 | import org.testng.annotations.Test; 10 | 11 | import java.io.IOException; 12 | import java.lang.reflect.Field; 13 | import java.util.Arrays; 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | import static org.junit.Assert.assertTrue; 18 | import static org.junit.Assert.fail; 19 | import static org.mockito.Matchers.any; 20 | import static org.mockito.Mockito.doAnswer; 21 | import static org.mockito.Mockito.doCallRealMethod; 22 | import static org.mockito.Mockito.mock; 23 | import static org.mockito.Mockito.verify; 24 | import static org.testng.Assert.assertEquals; 25 | import static org.testng.Assert.assertFalse; 26 | 27 | public class EmailFormatterTest { 28 | private static T get(Object target, String name, Class clazz) { 29 | try { 30 | Field f = target.getClass().getDeclaredField(name); 31 | f.setAccessible(true); 32 | Object o = f.get(target); 33 | return clazz.cast(o); 34 | } catch (Exception e) { 35 | throw new RuntimeException(e); 36 | } 37 | } 38 | 39 | private static void set(EmailFormatter target, String name, Object value) { 40 | Class cls = EmailFormatter.class; 41 | try { 42 | Field f = cls.getDeclaredField(name); 43 | f.setAccessible(true); 44 | f.set(target, value); 45 | } catch (Exception e) { 46 | throw new RuntimeException(e); 47 | } 48 | } 49 | 50 | @Test 51 | public void testSetup() { 52 | String[] args = { 53 | "--" + EmailFormatter.EMAIL_RECIPIENTS, "email@email.com", 54 | "--" + EmailFormatter.EMAIL_SENDER_NAME, "Validatar", 55 | "--" + EmailFormatter.EMAIL_FROM, "validatar@validatar.com", 56 | "--" + EmailFormatter.EMAIL_REPLY_TO, "validatar@validatar.com", 57 | "--" + EmailFormatter.EMAIL_SMTP_HOST, "host.host.com", 58 | "--" + EmailFormatter.EMAIL_SMTP_PORT, "25", 59 | "--" + EmailFormatter.EMAIL_SMTP_STRATEGY, "SMTP_PLAIN" 60 | }; 61 | EmailFormatter formatter = new EmailFormatter(); 62 | formatter.setup(args); 63 | List recipientEmails = get(formatter, "recipientEmails", List.class); 64 | assertEquals(recipientEmails.size(), 1); 65 | assertEquals(recipientEmails.get(0), "email@email.com"); 66 | assertEquals("Validatar", get(formatter, "senderName", String.class)); 67 | assertEquals("validatar@validatar.com", get(formatter, "fromEmail", String.class)); 68 | assertEquals("validatar@validatar.com", get(formatter, "replyTo", String.class)); 69 | assertEquals("host.host.com", get(formatter, "smtpHost", String.class)); 70 | assertEquals((Integer) 25, get(formatter, "smtpPort", Integer.class)); 71 | assertEquals(TransportStrategy.SMTP_PLAIN, get(formatter, "strategy", TransportStrategy.class)); 72 | } 73 | 74 | @Test 75 | public void testWriteReportShowsFailures() throws IOException { 76 | com.yahoo.validatar.common.Test test = new com.yahoo.validatar.common.Test(); 77 | com.yahoo.validatar.common.Test skipped = new com.yahoo.validatar.common.Test(); 78 | skipped.name = "SkippedTest"; 79 | skipped.warnOnly = true; 80 | skipped.addMessage("SkippedTestMessage"); 81 | Query query = new Query(); 82 | TestSuite ts = new TestSuite(); 83 | ts.name = "testSuiteName1"; 84 | test.name = "testName1"; 85 | query.name = "queryName1"; 86 | test.addMessage("testMessage1"); 87 | test.addMessage("testMessage2"); 88 | test.addMessage("testMessage3"); 89 | test.setFailed(); 90 | query.addMessage("queryMessage"); 91 | query.setFailed(); 92 | ts.queries = Collections.singletonList(query); 93 | ts.tests = Arrays.asList(test, skipped); 94 | EmailFormatter formatter = mock(EmailFormatter.class); 95 | doCallRealMethod().when(formatter).writeReport(any()); 96 | set(formatter, "recipientEmails", Collections.singletonList("email@email.com")); 97 | set(formatter, "senderName", "Validatar"); 98 | set(formatter, "fromEmail", "from@mail.com"); 99 | set(formatter, "replyTo", "reply@mail.com"); 100 | set(formatter, "smtpHost", "host.host.com"); 101 | set(formatter, "smtpPort", 25); 102 | doAnswer(iom -> { 103 | Email email = (Email) iom.getArguments()[1]; 104 | String html = email.getTextHTML(); 105 | String[] containsAllOf = { 106 | "testSuiteName1", "testName1", "queryName1", "testMessage1", "SkippedTestMessage", 107 | "testMessage2", "testMessage3", "queryMessage", "SKIPPED", "SkippedTest" 108 | }; 109 | for (String str : containsAllOf) { 110 | assertTrue(html.contains(str)); 111 | } 112 | return null; 113 | } 114 | ).when(formatter).sendEmail(any(), any()); 115 | formatter.writeReport(Collections.singletonList(ts)); 116 | verify(formatter).sendEmail(any(), any()); 117 | } 118 | 119 | @Test 120 | public void testWriteReportPassesAndShowsMessagesWhenOnlyWarnings() throws IOException { 121 | com.yahoo.validatar.common.Test test = new com.yahoo.validatar.common.Test(); 122 | com.yahoo.validatar.common.Test skipped = new com.yahoo.validatar.common.Test(); 123 | skipped.name = "SkippedTest"; 124 | skipped.warnOnly = true; 125 | skipped.addMessage("SkippedTestMessage"); 126 | Query query = new Query(); 127 | TestSuite ts = new TestSuite(); 128 | ts.name = "testSuiteName1"; 129 | test.name = "testName1"; 130 | query.name = "queryName1"; 131 | test.addMessage("testMessage1"); 132 | test.addMessage("testMessage2"); 133 | test.addMessage("testMessage3"); 134 | query.addMessage("queryMessage"); 135 | ts.queries = Collections.singletonList(query); 136 | ts.tests = Arrays.asList(test, skipped); 137 | EmailFormatter formatter = mock(EmailFormatter.class); 138 | doCallRealMethod().when(formatter).writeReport(any()); 139 | set(formatter, "recipientEmails", Collections.singletonList("email@email.com")); 140 | set(formatter, "senderName", "Validatar"); 141 | set(formatter, "fromEmail", "from@mail.com"); 142 | set(formatter, "replyTo", "reply@mail.com"); 143 | set(formatter, "smtpHost", "host.host.com"); 144 | set(formatter, "smtpPort", 25); 145 | doAnswer(iom -> { 146 | Email email = (Email) iom.getArguments()[1]; 147 | String html = email.getTextHTML(); 148 | String[] containsAllOf = { 149 | "SkippedTest", "SkippedTestMessage", "testSuiteName1" 150 | }; 151 | String[] containsNoneOf = { 152 | "testMessage1", "testMessage2", "testMessage3", "queryMessage", 153 | "testName1", "queryName1" 154 | }; 155 | for (String str : containsAllOf) { 156 | assertTrue(html.contains(str)); 157 | } 158 | for (String str : containsNoneOf) { 159 | assertFalse(html.contains(str)); 160 | } 161 | return null; 162 | } 163 | ).when(formatter).sendEmail(any(), any()); 164 | formatter.writeReport(Collections.singletonList(ts)); 165 | verify(formatter).sendEmail(any(), any()); 166 | } 167 | 168 | @Test 169 | public void testSetupReturnsFailMissingParams() { 170 | EmailFormatter formatter = new EmailFormatter(); 171 | assertFalse(formatter.setup(new String[]{})); 172 | } 173 | 174 | @Test 175 | public void testWriteReportEmptyTestSuites() throws IOException { 176 | EmailFormatter formatter = mock(EmailFormatter.class); 177 | doCallRealMethod().when(formatter).writeReport(any()); 178 | set(formatter, "recipientEmails", Collections.singletonList("email@email.com")); 179 | set(formatter, "senderName", "Validatar"); 180 | set(formatter, "fromEmail", "from@mail.com"); 181 | set(formatter, "replyTo", "reply@mail.com"); 182 | set(formatter, "smtpHost", "host.host.com"); 183 | set(formatter, "smtpPort", 25); 184 | doAnswer(iom -> { 185 | Email email = (Email) iom.getArguments()[1]; 186 | String html = email.getTextHTML(); 187 | assertTrue(html.contains("Nice!")); 188 | return null; 189 | } 190 | ).when(formatter).sendEmail(any(), any()); 191 | formatter.writeReport(null); 192 | } 193 | 194 | @Test 195 | public void testSendEmail() { 196 | Mailer mailer = mock(Mailer.class); 197 | Email email = mock(Email.class); 198 | EmailFormatter formatter = new EmailFormatter(); 199 | formatter.sendEmail(mailer, email); 200 | verify(mailer).sendMail(email); 201 | } 202 | 203 | @Test 204 | public void testGetName() { 205 | EmailFormatter formatter = new EmailFormatter(); 206 | assertEquals(EmailFormatter.EMAIL_FORMATTER, formatter.getName()); 207 | OutputCaptor.redirectToDevNull(); 208 | try { 209 | formatter.printHelp(); 210 | } catch (Exception e) { 211 | OutputCaptor.redirectToStandard(); 212 | fail(); 213 | } 214 | OutputCaptor.redirectToStandard(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/com/yahoo/validatar/common/Operators.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Yahoo Inc. 3 | * Licensed under the terms of the Apache 2 license. Please see LICENSE file in the project root for terms. 4 | */ 5 | package com.yahoo.validatar.common; 6 | 7 | import java.math.BigDecimal; 8 | import java.sql.Timestamp; 9 | 10 | import static com.yahoo.validatar.common.TypeSystem.asTypedObject; 11 | 12 | /** 13 | * Contains the various type specific {@link Operations}. 14 | *

15 | * In general, we don't want lossy casting, or strange casting like a boolean to a short etc. 16 | * But we will follow the basic Java widening primitive rules. 17 | * https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html 18 | *

19 | * Exceptions: 20 | * Timestamp to and from Long will do a millis since epoch 21 | */ 22 | public class Operators { 23 | public static class BooleanOperator implements Operations { 24 | @Override 25 | public TypedObject or(TypedObject first, TypedObject second) { 26 | return asTypedObject((Boolean) first.data || (Boolean) second.data); 27 | } 28 | 29 | @Override 30 | public TypedObject and(TypedObject first, TypedObject second) { 31 | return asTypedObject((Boolean) first.data && (Boolean) second.data); 32 | } 33 | 34 | @Override 35 | public TypedObject not(TypedObject object) { 36 | return asTypedObject(!(Boolean) object.data); 37 | } 38 | 39 | @Override 40 | public TypedObject cast(TypedObject object) { 41 | switch (object.type) { 42 | case STRING: 43 | object.data = Boolean.valueOf((String) object.data); 44 | break; 45 | case BOOLEAN: 46 | break; 47 | case LONG: 48 | case DOUBLE: 49 | case DECIMAL: 50 | case TIMESTAMP: 51 | return null; 52 | } 53 | object.type = TypeSystem.Type.BOOLEAN; 54 | return object; 55 | } 56 | } 57 | 58 | public static class LongOperator implements Operations { 59 | @Override 60 | public TypedObject add(TypedObject first, TypedObject second) { 61 | return asTypedObject((Long) first.data + (Long) second.data); 62 | } 63 | 64 | @Override 65 | public TypedObject subtract(TypedObject first, TypedObject second) { 66 | return asTypedObject((Long) first.data - (Long) second.data); 67 | } 68 | 69 | @Override 70 | public TypedObject multiply(TypedObject first, TypedObject second) { 71 | return asTypedObject((Long) first.data * (Long) second.data); 72 | } 73 | 74 | @Override 75 | public TypedObject divide(TypedObject first, TypedObject second) { 76 | return asTypedObject((Long) first.data / (Long) second.data); 77 | } 78 | 79 | @Override 80 | public TypedObject modulus(TypedObject first, TypedObject second) { 81 | return asTypedObject((Long) first.data % (Long) second.data); 82 | } 83 | 84 | @Override 85 | public TypedObject cast(TypedObject object) { 86 | switch (object.type) { 87 | case STRING: 88 | object.data = Long.valueOf((String) object.data); 89 | break; 90 | case LONG: 91 | break; 92 | case TIMESTAMP: 93 | object.data = ((Timestamp) object.data).getTime(); 94 | break; 95 | case DOUBLE: 96 | case DECIMAL: 97 | case BOOLEAN: 98 | return null; 99 | } 100 | object.type = TypeSystem.Type.LONG; 101 | return object; 102 | } 103 | } 104 | public static class DoubleOperator implements Operations { 105 | @Override 106 | public TypedObject add(TypedObject first, TypedObject second) { 107 | return asTypedObject((Double) first.data + (Double) second.data); 108 | } 109 | 110 | @Override 111 | public TypedObject subtract(TypedObject first, TypedObject second) { 112 | return asTypedObject((Double) first.data - (Double) second.data); 113 | } 114 | 115 | @Override 116 | public TypedObject multiply(TypedObject first, TypedObject second) { 117 | return asTypedObject((Double) first.data * (Double) second.data); 118 | } 119 | 120 | @Override 121 | public TypedObject divide(TypedObject first, TypedObject second) { 122 | return asTypedObject((Double) first.data / (Double) second.data); 123 | } 124 | 125 | @Override 126 | public TypedObject cast(TypedObject object) { 127 | switch (object.type) { 128 | case STRING: 129 | object.data = Double.valueOf((String) object.data); 130 | break; 131 | case DOUBLE: 132 | break; 133 | case LONG: 134 | object.data = ((Long) object.data).doubleValue(); 135 | break; 136 | case DECIMAL: 137 | case BOOLEAN: 138 | case TIMESTAMP: 139 | return null; 140 | } 141 | object.type = TypeSystem.Type.DOUBLE; 142 | return object; 143 | } 144 | } 145 | 146 | public static class StringOperator implements Operations { 147 | @Override 148 | public TypedObject add(TypedObject first, TypedObject second) { 149 | return asTypedObject((String) first.data + (String) second.data); 150 | } 151 | 152 | @Override 153 | public TypedObject cast(TypedObject object) { 154 | switch (object.type) { 155 | case STRING: 156 | break; 157 | case LONG: 158 | object.data = ((Long) object.data).toString(); 159 | break; 160 | case DOUBLE: 161 | object.data = ((Double) object.data).toString(); 162 | break; 163 | case DECIMAL: 164 | object.data = ((BigDecimal) object.data).toString(); 165 | break; 166 | case BOOLEAN: 167 | object.data = ((Boolean) object.data).toString(); 168 | break; 169 | case TIMESTAMP: 170 | return null; 171 | } 172 | object.type = TypeSystem.Type.STRING; 173 | return object; 174 | } 175 | } 176 | 177 | public static class DecimalOperator implements Operations { 178 | @Override 179 | public TypedObject add(TypedObject first, TypedObject second) { 180 | return asTypedObject(((BigDecimal) first.data).add((BigDecimal) second.data)); 181 | } 182 | 183 | @Override 184 | public TypedObject subtract(TypedObject first, TypedObject second) { 185 | return asTypedObject(((BigDecimal) first.data).subtract((BigDecimal) second.data)); 186 | } 187 | 188 | @Override 189 | public TypedObject multiply(TypedObject first, TypedObject second) { 190 | return asTypedObject(((BigDecimal) first.data).multiply((BigDecimal) second.data)); 191 | } 192 | 193 | @Override 194 | public TypedObject divide(TypedObject first, TypedObject second) { 195 | return asTypedObject(((BigDecimal) first.data).divide((BigDecimal) second.data)); 196 | } 197 | 198 | @Override 199 | public TypedObject modulus(TypedObject first, TypedObject second) { 200 | return asTypedObject(((BigDecimal) first.data).divideAndRemainder((BigDecimal) second.data)[1]); 201 | } 202 | 203 | @Override 204 | public TypedObject cast(TypedObject object) { 205 | switch (object.type) { 206 | case STRING: 207 | object.data = new BigDecimal((String) object.data); 208 | break; 209 | case LONG: 210 | object.data = BigDecimal.valueOf((Long) object.data); 211 | break; 212 | case DOUBLE: 213 | object.data = BigDecimal.valueOf((Double) object.data); 214 | break; 215 | case DECIMAL: 216 | break; 217 | case TIMESTAMP: 218 | object.data = BigDecimal.valueOf(((Timestamp) object.data).getTime()); 219 | break; 220 | case BOOLEAN: 221 | return null; 222 | } 223 | object.type = TypeSystem.Type.DECIMAL; 224 | return object; 225 | } 226 | } 227 | 228 | public static class TimestampOperator implements Operations { 229 | @Override 230 | public TypedObject add(TypedObject first, TypedObject second) { 231 | return asTypedObject(new Timestamp(((Timestamp) first.data).getTime() + ((Timestamp) second.data).getTime())); 232 | } 233 | 234 | @Override 235 | public TypedObject subtract(TypedObject first, TypedObject second) { 236 | return asTypedObject(new Timestamp(((Timestamp) first.data).getTime() - ((Timestamp) second.data).getTime())); 237 | } 238 | 239 | @Override 240 | public TypedObject multiply(TypedObject first, TypedObject second) { 241 | return asTypedObject(new Timestamp(((Timestamp) first.data).getTime() * ((Timestamp) second.data).getTime())); 242 | } 243 | 244 | @Override 245 | public TypedObject divide(TypedObject first, TypedObject second) { 246 | return asTypedObject(new Timestamp(((Timestamp) first.data).getTime() / ((Timestamp) second.data).getTime())); 247 | } 248 | 249 | @Override 250 | public TypedObject modulus(TypedObject first, TypedObject second) { 251 | return asTypedObject(new Timestamp(((Timestamp) first.data).getTime() % ((Timestamp) second.data).getTime())); 252 | } 253 | 254 | @Override 255 | public TypedObject cast(TypedObject object) { 256 | switch (object.type) { 257 | case LONG: 258 | object.data = new Timestamp((Long) object.data); 259 | break; 260 | case TIMESTAMP: 261 | break; 262 | case STRING: 263 | case DOUBLE: 264 | case DECIMAL: 265 | case BOOLEAN: 266 | return null; 267 | } 268 | object.type = TypeSystem.Type.TIMESTAMP; 269 | return object; 270 | } 271 | } 272 | } 273 | --------------------------------------------------------------------------------