diff --git a/build.gradle b/build.gradle index 5bdaa0561e..969ff850c4 100644 --- a/build.gradle +++ b/build.gradle @@ -44,13 +44,30 @@ jacocoTestReport { dependencies { implementation 'com.fulcrumgenomics:jlibdeflate:0.1.0' - implementation 'commons-logging:commons-logging:1.3.0' implementation "org.xerial.snappy:snappy-java:1.1.10.5" implementation 'org.apache.commons:commons-compress:1.26.0' implementation 'org.tukaani:xz:1.9' implementation "org.json:json:20231013" - implementation 'org.openjdk.nashorn:nashorn-core:15.4' - + + // commons-jexl 2.1.1 pulls commons-logging:1.1.1 (released 2007). htsjdk has no direct + // need for commons-logging itself, so we publish a version constraint rather than a real + // dependency: it kicks in only if commons-logging is pulled transitively, and bumps it to + // a maintained version. Drop this if commons-jexl is ever upgraded past 2.1.1. + constraints { + implementation('commons-logging:commons-logging:1.3.0') { + because 'jexl 2.1.1 pulls commons-logging 1.1.1 transitively; pin a maintained version' + } + } + + // Nashorn is the JSR-223 "js" engine used by the optional JavaScript filter classes + // (htsjdk.samtools.filter.JavascriptSamRecordFilter, htsjdk.variant.variantcontext.filter.JavascriptVariantFilter). + // It's compileOnly so downstream consumers who don't use those filter classes don't pay the + // cost of nashorn-core + 5 ASM artifacts on their runtime classpath. Consumers who do use + // them must add nashorn-core to their own runtime classpath; see the error message thrown + // by AbstractJavascriptFilter when no JS engine is found. + compileOnly 'org.openjdk.nashorn:nashorn-core:15.7' + testImplementation 'org.openjdk.nashorn:nashorn-core:15.7' + api "org.apache.commons:commons-jexl:2.1.1" testImplementation 'org.testng:testng:7.8.0' diff --git a/src/main/java/htsjdk/samtools/filter/AbstractJavascriptFilter.java b/src/main/java/htsjdk/samtools/filter/AbstractJavascriptFilter.java index 5a2668d2c4..a74c96d1c7 100644 --- a/src/main/java/htsjdk/samtools/filter/AbstractJavascriptFilter.java +++ b/src/main/java/htsjdk/samtools/filter/AbstractJavascriptFilter.java @@ -85,8 +85,7 @@ protected AbstractJavascriptFilter(final Reader scriptReader, final HEADER heade final ScriptEngine engine = manager.getEngineByName("js"); if (engine == null) { CloserUtil.close(scriptReader); - throw new RuntimeScriptException("The embedded 'javascript' engine is not available in java. " - + "Do you use the SUN/Oracle Java Runtime ?"); + throw new RuntimeScriptException(noJsEngineMessage(this.getClass().getSimpleName())); } if (scriptReader == null) { throw new RuntimeScriptException("missing ScriptReader."); @@ -109,6 +108,30 @@ protected AbstractJavascriptFilter(final Reader scriptReader, final HEADER heade this.bindings.put(DEFAULT_HEADER_KEY, header); } + static String noJsEngineMessage(final String filterClassName) { + return String.join( + "\n", + "No JSR-223 JavaScript engine (lookup name \"js\") was found on the classpath.", + "", + "Starting with htsjdk 5.0.0, htsjdk no longer ships a JavaScript engine as a runtime", + "dependency, so that consumers who do not use the JavaScript filter classes do not pay", + "the cost of carrying ~6 extra jars (nashorn-core plus its ASM transitives, ~2.5 MB).", + "", + "To use " + filterClassName + ", add a JSR-223-compatible JavaScript engine to your", + "runtime classpath. The recommended choice is OpenJDK Nashorn:", + "", + " Gradle: runtimeOnly 'org.openjdk.nashorn:nashorn-core:15.7'", + "", + " Maven: ", + " org.openjdk.nashorn", + " nashorn-core", + " 15.7", + " runtime", + " ", + "", + "Any other JSR-223 engine that registers under the name \"js\" will also work."); + } + /** return a javascript engine as a Compilable */ private static Compilable getCompilable(final ScriptEngine engine) { if (!(engine instanceof Compilable)) { diff --git a/src/main/java/htsjdk/samtools/filter/JavascriptSamRecordFilter.java b/src/main/java/htsjdk/samtools/filter/JavascriptSamRecordFilter.java index 2b10a99eb0..0a452dc3d9 100644 --- a/src/main/java/htsjdk/samtools/filter/JavascriptSamRecordFilter.java +++ b/src/main/java/htsjdk/samtools/filter/JavascriptSamRecordFilter.java @@ -30,16 +30,26 @@ import java.io.Reader; /** - * javascript based read filter + * JavaScript-based {@link SamRecordFilter}. * + *

The user-supplied script is evaluated against each {@link SAMRecord} with the following + * variables in scope: * - * The script puts the following variables in the script context: + *

* - * - 'record' a SamRecord ( - * https://github.com/samtools/htsjdk/blob/master/src/java/htsjdk/samtools/ - * SAMRecord.java ) - 'header' ( - * https://github.com/samtools/htsjdk/blob/master/src/java/htsjdk/samtools/ - * SAMFileHeader.java ) + *

Example: keep only records with mapping quality >= 30: + *

{@code
+ *     new JavascriptSamRecordFilter("record.getMappingQuality() >= 30;", header)
+ * }
+ * + *

Runtime requirement: as of htsjdk 5.0.0, htsjdk does not ship a JavaScript engine as + * a runtime dependency. To use this class, add a JSR-223-compatible JavaScript engine + * (e.g. {@code org.openjdk.nashorn:nashorn-core}) to your runtime classpath. If no engine is + * available, the constructor throws a {@link htsjdk.samtools.util.RuntimeScriptException} whose + * message lists the dependency coordinates. * * @author Pierre Lindenbaum PhD Institut du Thorax - INSERM - Nantes - France */ diff --git a/src/main/java/htsjdk/variant/variantcontext/filter/JavascriptVariantFilter.java b/src/main/java/htsjdk/variant/variantcontext/filter/JavascriptVariantFilter.java index dedfaef253..61661754d8 100644 --- a/src/main/java/htsjdk/variant/variantcontext/filter/JavascriptVariantFilter.java +++ b/src/main/java/htsjdk/variant/variantcontext/filter/JavascriptVariantFilter.java @@ -31,11 +31,26 @@ import java.io.Reader; /** - * javascript based variant filter The script puts the following variables in - * the script context: + * JavaScript-based {@link VariantContextFilter}. * - * - 'header' a htsjdk.variant.vcf.VCFHeader - * - 'variant' a htsjdk.variant.variantcontext.VariantContext + *

The user-supplied script is evaluated against each {@link VariantContext} with the following + * variables in scope: + * + *

+ * + *

Example: keep only variants on chromosome 1: + *

{@code
+ *     new JavascriptVariantFilter("variant.getContig() == '1';", header)
+ * }
+ * + *

Runtime requirement: as of htsjdk 5.0.0, htsjdk does not ship a JavaScript engine as + * a runtime dependency. To use this class, add a JSR-223-compatible JavaScript engine + * (e.g. {@code org.openjdk.nashorn:nashorn-core}) to your runtime classpath. If no engine is + * available, the constructor throws a {@link htsjdk.samtools.util.RuntimeScriptException} whose + * message lists the dependency coordinates. * * @author Pierre Lindenbaum PhD Institut du Thorax - INSERM - Nantes - France */ diff --git a/src/test/java/htsjdk/samtools/filter/JavascriptSamRecordFilterTest.java b/src/test/java/htsjdk/samtools/filter/JavascriptSamRecordFilterTest.java index 6e15f96571..0a4958260c 100644 --- a/src/test/java/htsjdk/samtools/filter/JavascriptSamRecordFilterTest.java +++ b/src/test/java/htsjdk/samtools/filter/JavascriptSamRecordFilterTest.java @@ -24,50 +24,266 @@ package htsjdk.samtools.filter; import htsjdk.HtsjdkTest; -import htsjdk.samtools.SAMRecordIterator; -import htsjdk.samtools.SamReader; -import htsjdk.samtools.SamReaderFactory; -import htsjdk.samtools.util.CloserUtil; +import htsjdk.samtools.SAMFileHeader; +import htsjdk.samtools.SAMRecord; +import htsjdk.samtools.SAMSequenceRecord; +import htsjdk.samtools.util.RuntimeScriptException; import java.io.File; import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; import org.testng.Assert; -import org.testng.annotations.DataProvider; import org.testng.annotations.Test; /** - * @author Pierre Lindenbaum PhD Institut du Thorax - INSERM - Nantes - France + * Unit tests for {@link JavascriptSamRecordFilter}. Tests are written as many small, focused + * cases against inline String scripts so the script body is visible next to the assertion. + * + *

The base contract being tested ({@link AbstractJavascriptFilter#accept}): + *

+ * (here "accept" / "reject" describe what {@code accept()} returns; {@code filterOut()} negates). */ public class JavascriptSamRecordFilterTest extends HtsjdkTest { - final File testDir = new File("./src/test/resources/htsjdk/samtools"); - @DataProvider - public Object[][] jsData() { - return new Object[][] { - {"unsorted.sam", "samFilter01.js", 8}, {"unsorted.sam", "samFilter02.js", 10}, - }; + // --- helpers ---------------------------------------------------------------------------------- + + /** A minimal header with one sequence "chr1" of length 1000. */ + private static SAMFileHeader header() { + SAMFileHeader h = new SAMFileHeader(); + h.addSequence(new SAMSequenceRecord("chr1", 1000)); + return h; + } + + /** Build a mapped fragment record on chr1 at the given start with the given read name and bases. */ + private static SAMRecord record(SAMFileHeader h, String name, int start, String bases) { + SAMRecord r = new SAMRecord(h); + r.setReadName(name); + r.setReferenceName("chr1"); + r.setAlignmentStart(start); + r.setReadString(bases); + r.setBaseQualityString("I".repeat(bases.length())); + r.setMappingQuality(60); + r.setCigarString(bases.length() + "M"); + r.setReadUnmappedFlag(false); + return r; + } + + /** True iff the given script accepts the given record (i.e. filterOut returns false). */ + private static boolean accepts(String script, SAMFileHeader h, SAMRecord r) { + return !new JavascriptSamRecordFilter(script, h).filterOut(r); + } + + // --- constructors ----------------------------------------------------------------------------- + + @Test + public void stringConstructor_compilesAndRuns() { + Assert.assertTrue(accepts("true;", header(), record(header(), "r", 100, "AAAA"))); + } + + @Test + public void readerConstructor_compilesAndRuns() { + SAMFileHeader h = header(); + JavascriptSamRecordFilter f = new JavascriptSamRecordFilter(new StringReader("true;"), h); + Assert.assertFalse(f.filterOut(record(h, "r", 100, "AAAA"))); + } + + @Test + public void fileConstructor_compilesAndRuns() throws IOException { + File scriptFile = File.createTempFile("filter", ".js"); + scriptFile.deleteOnExit(); + Files.writeString(scriptFile.toPath(), "true;", StandardCharsets.UTF_8); + SAMFileHeader h = header(); + JavascriptSamRecordFilter f = new JavascriptSamRecordFilter(scriptFile, h); + Assert.assertFalse(f.filterOut(record(h, "r", 100, "AAAA"))); + } + + // --- return-type semantics -------------------------------------------------------------------- + + @Test + public void scriptReturnsTrue_isAccepted() { + Assert.assertTrue(accepts("true;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsFalse_isRejected() { + Assert.assertFalse(accepts("false;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsOne_isAccepted() { + Assert.assertTrue(accepts("1;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsZero_isRejected() { + Assert.assertFalse(accepts("0;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsTwo_isRejected() { + // accept() requires intValue() == 1, not "any truthy number". + Assert.assertFalse(accepts("2;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsNegativeOne_isRejected() { + Assert.assertFalse(accepts("-1;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsOnePointZero_isAccepted() { + // 1.0 has intValue() == 1, so this should be accepted under the documented contract. + Assert.assertTrue(accepts("1.0;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsNull_isRejected() { + Assert.assertFalse(accepts("null;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsUndefined_isRejected() { + Assert.assertFalse(accepts("undefined;", header(), record(header(), "r", 100, "A"))); + } + + @Test + public void scriptReturnsString_isRejected() { + // Non-null, non-Boolean, non-Number falls into the "anything else" branch -> reject. + Assert.assertFalse(accepts("'hello';", header(), record(header(), "r", 100, "A"))); } - @Test(dataProvider = "jsData") - public void testJavascriptFilters(final String samFile, final String javascriptFile, final int expectCount) { - final SamReaderFactory srf = SamReaderFactory.makeDefault(); - final SamReader samReader = srf.open(new File(testDir, samFile)); - final JavascriptSamRecordFilter filter; - try { - filter = new JavascriptSamRecordFilter(new File(testDir, javascriptFile), samReader.getFileHeader()); - } catch (IOException err) { - Assert.fail("Cannot read script", err); - return; - } - final SAMRecordIterator iter = samReader.iterator(); - int count = 0; - while (iter.hasNext()) { - if (filter.filterOut(iter.next())) { - continue; + // --- bindings: record key --------------------------------------------------------------------- + + @Test + public void scriptCanCallMethodsOnRecord() { + SAMFileHeader h = header(); + Assert.assertTrue(accepts("record.getReadName() == 'target';", h, record(h, "target", 100, "A"))); + Assert.assertFalse(accepts("record.getReadName() == 'target';", h, record(h, "other", 100, "A"))); + } + + @Test + public void scriptCanReadAlignmentStart() { + SAMFileHeader h = header(); + Assert.assertTrue(accepts("record.getAlignmentStart() == 250;", h, record(h, "r", 250, "A"))); + Assert.assertFalse(accepts("record.getAlignmentStart() == 250;", h, record(h, "r", 100, "A"))); + } + + @Test + public void scriptCanReadReadString() { + SAMFileHeader h = header(); + SAMRecord r = record(h, "r", 100, "ACGT"); + Assert.assertTrue(accepts("record.getReadString() == 'ACGT';", h, r)); + } + + // --- bindings: header key --------------------------------------------------------------------- + + @Test + public void scriptCanReachHeader() { + SAMFileHeader h = header(); + // header is bound under "header"; ensure it's reachable and methods work. + Assert.assertTrue(accepts("header.getSequenceDictionary().size() == 1;", h, record(h, "r", 100, "A"))); + } + + @Test + public void scriptCanReadSequenceFromHeader() { + SAMFileHeader h = header(); + Assert.assertTrue(accepts( + "header.getSequenceDictionary().getSequence(0).getSequenceName() == 'chr1';", + h, + record(h, "r", 100, "A"))); + } + + // --- subclass record-key override ------------------------------------------------------------- + + @Test + public void subclassCanOverrideRecordKey() { + SAMFileHeader h = header(); + // Subclass that exposes the record under "rec" instead of "record". + JavascriptSamRecordFilter f = new JavascriptSamRecordFilter("rec.getReadName() == 'r';", h) { + @Override + public String getRecordKey() { + return "rec"; } - ++count; - } - iter.close(); - CloserUtil.close(samReader); - Assert.assertEquals(count, expectCount, "Expected number of reads " + expectCount + " but got " + count); + }; + Assert.assertFalse(f.filterOut(record(h, "r", 100, "A"))); + Assert.assertTrue(f.filterOut(record(h, "other", 100, "A"))); + } + + // --- error paths ------------------------------------------------------------------------------ + + @Test(expectedExceptions = RuntimeScriptException.class) + public void malformedScript_throwsAtConstruction() { + new JavascriptSamRecordFilter("this is not javascript ;)", header()); + } + + @Test(expectedExceptions = RuntimeException.class) + public void scriptThatThrowsAtEval_propagatesAsRuntimeException() { + SAMFileHeader h = header(); + JavascriptSamRecordFilter f = new JavascriptSamRecordFilter("throw 'boom';", h); + f.filterOut(record(h, "r", 100, "A")); + } + + // --- filterOut(first, second) AND-semantics --------------------------------------------------- + + @Test + public void filterOutPair_bothRejected_isFilteredOut() { + SAMFileHeader h = header(); + // "rejected" means accept() returns false, i.e. filterOut() returns true. + // Script rejects everything -> both reject -> AND -> true. + JavascriptSamRecordFilter f = new JavascriptSamRecordFilter("false;", h); + Assert.assertTrue(f.filterOut(record(h, "a", 100, "A"), record(h, "b", 200, "A"))); + } + + @Test + public void filterOutPair_oneAccepted_isKept() { + // "kept" means filterOut returns false, i.e. accept-or branch wins. + SAMFileHeader h = header(); + // Script accepts only reads named "a"; pair has one of each -> AND of (false, true) -> false -> kept. + JavascriptSamRecordFilter f = new JavascriptSamRecordFilter("record.getReadName() == 'a';", h); + Assert.assertFalse(f.filterOut(record(h, "a", 100, "A"), record(h, "b", 200, "A"))); + } + + @Test + public void filterOutPair_bothAccepted_isKept() { + SAMFileHeader h = header(); + JavascriptSamRecordFilter f = new JavascriptSamRecordFilter("true;", h); + Assert.assertFalse(f.filterOut(record(h, "a", 100, "A"), record(h, "b", 200, "A"))); + } + + // --- multi-record reuse ----------------------------------------------------------------------- + + // --- error message for the no-engine path ---------------------------------------------------- + + @Test + public void noJsEngineMessage_containsActionableInfo() { + // The "no engine on classpath" error is the primary UX for downstream consumers who + // forget the runtime dep, so verify it stays actionable. + String msg = AbstractJavascriptFilter.noJsEngineMessage("JavascriptSamRecordFilter"); + Assert.assertTrue(msg.contains("JavascriptSamRecordFilter"), "message should name the filter class"); + Assert.assertTrue(msg.contains("nashorn-core"), "message should name the recommended artifact"); + Assert.assertTrue(msg.contains("Gradle"), "message should show how to add it via Gradle"); + Assert.assertTrue(msg.contains("Maven"), "message should show how to add it via Maven"); + } + + @Test + public void filterRefreshesBindingsAcrossManyRecords() { + SAMFileHeader h = header(); + JavascriptSamRecordFilter f = new JavascriptSamRecordFilter("record.getAlignmentStart() >= 200;", h); + + List records = new ArrayList<>(); + for (int i = 1; i <= 4; i++) records.add(record(h, "r" + i, i * 100, "A")); + + // Expected: r1@100 -> reject, r2@200/r3@300/r4@400 -> accept. + int kept = 0; + for (SAMRecord r : records) if (!f.filterOut(r)) kept++; + Assert.assertEquals(kept, 3); } } diff --git a/src/test/java/htsjdk/variant/variantcontext/filter/JavascriptVariantFilterTest.java b/src/test/java/htsjdk/variant/variantcontext/filter/JavascriptVariantFilterTest.java index b1538745fe..38015e8178 100644 --- a/src/test/java/htsjdk/variant/variantcontext/filter/JavascriptVariantFilterTest.java +++ b/src/test/java/htsjdk/variant/variantcontext/filter/JavascriptVariantFilterTest.java @@ -24,48 +24,206 @@ package htsjdk.variant.variantcontext.filter; import htsjdk.HtsjdkTest; -import htsjdk.variant.vcf.VCFFileReader; +import htsjdk.samtools.util.RuntimeScriptException; +import htsjdk.variant.variantcontext.VariantContext; +import htsjdk.variant.variantcontext.VariantContextBuilder; +import htsjdk.variant.vcf.VCFContigHeaderLine; +import htsjdk.variant.vcf.VCFHeader; +import htsjdk.variant.vcf.VCFHeaderLine; import java.io.File; import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.testng.Assert; -import org.testng.annotations.DataProvider; import org.testng.annotations.Test; /** - * @author Pierre Lindenbaum PhD Institut du Thorax - INSERM - Nantes - France + * Unit tests for {@link JavascriptVariantFilter}. Mirrors the structure of + * {@code JavascriptSamRecordFilterTest}: short, focused tests written against inline String scripts + * so the body is visible next to the assertion. + * + *

The base contract being tested ({@link htsjdk.samtools.filter.AbstractJavascriptFilter#accept}): + *

*/ public class JavascriptVariantFilterTest extends HtsjdkTest { - final File testDir = new File("src/test/resources/htsjdk/variant"); - - @DataProvider - public Object[][] jsData() { - return new Object[][] { - {"ILLUMINA.wex.broad_phase2_baseline.20111114.both.exome.genotypes.1000.vcf", "variantFilter01.js", 61}, - {"ILLUMINA.wex.broad_phase2_baseline.20111114.both.exome.genotypes.1000.vcf", "variantFilter02.js", 38}, - }; - } - - @Test(dataProvider = "jsData") - public void testJavascriptFilters(final String vcfFile, final String javascriptFile, final int expectCount) { - final File vcfInput = new File(testDir, vcfFile); - final File jsInput = new File(testDir, javascriptFile); - final VCFFileReader vcfReader = new VCFFileReader(vcfInput, false); - final JavascriptVariantFilter filter; - try { - filter = new JavascriptVariantFilter(jsInput, vcfReader.getFileHeader()); - } catch (IOException err) { - Assert.fail("cannot read script " + jsInput, err); - vcfReader.close(); - return; - } - final FilteringVariantContextIterator iter = new FilteringVariantContextIterator(vcfReader.iterator(), filter); - int count = 0; - while (iter.hasNext()) { - iter.next(); - ++count; - } - iter.close(); - vcfReader.close(); - Assert.assertEquals(count, expectCount, "Expected number of variants " + expectCount + " but got " + count); + + // --- helpers ---------------------------------------------------------------------------------- + + /** A minimal header with one contig "chr1" of length 1000 and no samples. */ + private static VCFHeader header() { + Map contigFields = new LinkedHashMap<>(); + contigFields.put("ID", "chr1"); + contigFields.put("length", "1000"); + Set meta = new LinkedHashSet<>(); + meta.add(new VCFContigHeaderLine(contigFields, 0)); + return new VCFHeader(meta); + } + + /** Build a SNP variant on chr1 at the given position with the given REF/ALT alleles. */ + private static VariantContext snv(String contig, int pos, String ref, String alt) { + return new VariantContextBuilder() + .source("test") + .chr(contig) + .start(pos) + .stop(pos) + .alleles(ref, alt) + .make(); + } + + /** True iff the given script accepts the given variant. */ + private static boolean accepts(String script, VCFHeader h, VariantContext v) { + return new JavascriptVariantFilter(script, h).test(v); + } + + // --- constructors ----------------------------------------------------------------------------- + + @Test + public void stringConstructor_compilesAndRuns() { + Assert.assertTrue(accepts("true;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void readerConstructor_compilesAndRuns() throws IOException { + VCFHeader h = header(); + JavascriptVariantFilter f = new JavascriptVariantFilter(new StringReader("true;"), h); + Assert.assertTrue(f.test(snv("chr1", 100, "A", "C"))); + } + + @Test + public void fileConstructor_compilesAndRuns() throws IOException { + File scriptFile = File.createTempFile("filter", ".js"); + scriptFile.deleteOnExit(); + Files.writeString(scriptFile.toPath(), "true;", StandardCharsets.UTF_8); + VCFHeader h = header(); + JavascriptVariantFilter f = new JavascriptVariantFilter(scriptFile, h); + Assert.assertTrue(f.test(snv("chr1", 100, "A", "C"))); + } + + // --- return-type semantics -------------------------------------------------------------------- + + @Test + public void scriptReturnsTrue_isAccepted() { + Assert.assertTrue(accepts("true;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsFalse_isRejected() { + Assert.assertFalse(accepts("false;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsOne_isAccepted() { + Assert.assertTrue(accepts("1;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsZero_isRejected() { + Assert.assertFalse(accepts("0;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsTwo_isRejected() { + Assert.assertFalse(accepts("2;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsNegativeOne_isRejected() { + Assert.assertFalse(accepts("-1;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsOnePointZero_isAccepted() { + Assert.assertTrue(accepts("1.0;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsNull_isRejected() { + Assert.assertFalse(accepts("null;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsUndefined_isRejected() { + Assert.assertFalse(accepts("undefined;", header(), snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptReturnsString_isRejected() { + Assert.assertFalse(accepts("'hello';", header(), snv("chr1", 100, "A", "C"))); + } + + // --- bindings: variant key -------------------------------------------------------------------- + + @Test + public void scriptCanCallMethodsOnVariant() { + VCFHeader h = header(); + Assert.assertTrue(accepts("variant.getStart() == 100;", h, snv("chr1", 100, "A", "C"))); + Assert.assertFalse(accepts("variant.getStart() == 100;", h, snv("chr1", 250, "A", "C"))); + } + + @Test + public void scriptCanReadContig() { + VCFHeader h = header(); + Assert.assertTrue(accepts("variant.getContig() == 'chr1';", h, snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptCanReadAlleles() { + VCFHeader h = header(); + Assert.assertTrue(accepts("variant.getReference().getBaseString() == 'A';", h, snv("chr1", 100, "A", "C"))); + } + + // --- bindings: header key --------------------------------------------------------------------- + + @Test + public void scriptCanReachHeader() { + VCFHeader h = header(); + Assert.assertTrue(accepts("header.getContigLines().size() == 1;", h, snv("chr1", 100, "A", "C"))); + } + + @Test + public void scriptCanReadContigFromHeader() { + VCFHeader h = header(); + Assert.assertTrue(accepts("header.getContigLines().get(0).getID() == 'chr1';", h, snv("chr1", 100, "A", "C"))); + } + + // --- error paths ------------------------------------------------------------------------------ + + @Test(expectedExceptions = RuntimeScriptException.class) + public void malformedScript_throwsAtConstruction() { + new JavascriptVariantFilter("this is not javascript ;)", header()); + } + + @Test(expectedExceptions = RuntimeException.class) + public void scriptThatThrowsAtEval_propagatesAsRuntimeException() { + VCFHeader h = header(); + JavascriptVariantFilter f = new JavascriptVariantFilter("throw 'boom';", h); + f.test(snv("chr1", 100, "A", "C")); + } + + // --- multi-record reuse ----------------------------------------------------------------------- + + @Test + public void filterRefreshesBindingsAcrossManyVariants() { + VCFHeader h = header(); + JavascriptVariantFilter f = new JavascriptVariantFilter("variant.getStart() >= 200;", h); + + List variants = new ArrayList<>(); + for (int i = 1; i <= 4; i++) variants.add(snv("chr1", i * 100, "A", "C")); + + int kept = 0; + for (VariantContext v : variants) if (f.test(v)) kept++; + Assert.assertEquals(kept, 3); } } diff --git a/src/test/resources/htsjdk/samtools/samFilter01.js b/src/test/resources/htsjdk/samtools/samFilter01.js deleted file mode 100644 index 3fe7e00394..0000000000 --- a/src/test/resources/htsjdk/samtools/samFilter01.js +++ /dev/null @@ -1,2 +0,0 @@ -/** answer to https://www.biostars.org/p/77802/#77966 */ -(record.referenceIndex==record.mateReferenceIndex && record.referenceIndex>=0 && record.readNegativeStrandFlag!=record.mateNegativeStrandFlag && ((record.mateNegativeStrandFlag && record.alignmentStart < record.mateAlignmentStart ) || (record.readNegativeStrandFlag && record.mateAlignmentStart < record.alignmentStart ) )) diff --git a/src/test/resources/htsjdk/samtools/samFilter02.js b/src/test/resources/htsjdk/samtools/samFilter02.js deleted file mode 100644 index 046e7ecae4..0000000000 --- a/src/test/resources/htsjdk/samtools/samFilter02.js +++ /dev/null @@ -1,9 +0,0 @@ -/** accept record if second base of DNA is a A */ -function accept(r) - { - /* using substring instead of charAt because http://developer.actuate.com/community/forum/index.php?/topic/25434-javascript-stringcharati-wont-return-a-character/ */ - return r.getReadString().length()>2 && - r.getReadString().substring(1,2)=="A"; - } - -accept(record); diff --git a/src/test/resources/htsjdk/variant/variantFilter01.js b/src/test/resources/htsjdk/variant/variantFilter01.js deleted file mode 100644 index 0036477a86..0000000000 --- a/src/test/resources/htsjdk/variant/variantFilter01.js +++ /dev/null @@ -1,2 +0,0 @@ -/** get variant having position%2==0 */ -variant.getStart()%2 == 0; diff --git a/src/test/resources/htsjdk/variant/variantFilter02.js b/src/test/resources/htsjdk/variant/variantFilter02.js deleted file mode 100644 index c102d25291..0000000000 --- a/src/test/resources/htsjdk/variant/variantFilter02.js +++ /dev/null @@ -1,20 +0,0 @@ -/** prints a VARIATION if two samples at least have a DP>100 */ -function myfilterFunction(thevariant) - { - var samples=header.genotypeSamples; - var countOkDp=0; - - - for(var i=0; i< samples.size();++i) - { - var sampleName=samples.get(i); - if(! variant.hasGenotype(sampleName)) continue; - var genotype = thevariant.genotypes.get(sampleName); - if( ! genotype.hasDP()) continue; - var dp= genotype.getDP(); - if(dp > 100 ) countOkDp++; - } - return (countOkDp>2) - } - -myfilterFunction(variant)