diff --git a/documentation/src/command/THROWIF.rst b/documentation/src/command/THROWIF.rst index e9ab0451..12ba5d83 100644 --- a/documentation/src/command/THROWIF.rst +++ b/documentation/src/command/THROWIF.rst @@ -9,6 +9,9 @@ THROWIF ======= The **THROWIF** command is a very simple tool which throws an exception if the contained statement is true. +By default the error message is ``Gor throw on: ``. Use ``-m`` to give a custom message. The error also +includes the header and the row that triggered the exception. + Usage ===== @@ -21,4 +24,23 @@ Options +--------------------+---------------------------------------------------------------------------------------+ | ``-retriable`` | If set an retriable exception is thrown. | -+--------------------+---------------------------------------------------------------------------------------+ \ No newline at end of file ++--------------------+---------------------------------------------------------------------------------------+ +| ``-m `` | Use ```` as the error message instead of ``Gor throw on: ``. | +| | Quote the message if it contains spaces. | ++--------------------+---------------------------------------------------------------------------------------+ + +Examples +======== +Fail with a custom message: + +.. code-block:: gor + + gorrow chr1,1 | calc status 'unmapped' | THROWIF -m 'liftover failed' status != 'mapped' + +The error report contains (command source and stack trace omitted):: + + ==== Data Error ==== + liftover failed + + Header: chrom pos status + Row: chr1 1 unmapped \ No newline at end of file diff --git a/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala b/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala index 35368513..47d2891a 100644 --- a/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala +++ b/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala @@ -27,11 +27,20 @@ import org.gorpipe.exceptions.GorDataException import org.gorpipe.gor.model.Row import org.gorpipe.gor.session.GorContext -case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: String, header: String, isRetriable: Boolean = false) +/** + * Throws a GorDataException on the first row where filterSrc evaluates to true. The exception carries the + * header and the offending row. + * + * @param message error message to use instead of the default "Gor throw on: ", null for the default + */ +case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: String, header: String, + isRetriable: Boolean = false, message: String = null) extends Analysis with Filtering { filter.setContext(context, executeNor) + private val errorMessage = if (message == null) s"Gor throw on: $filterSrc" else message + override def isTypeInformationNeeded: Boolean = true override def isTypeInformationMaintained: Boolean = true @@ -52,7 +61,12 @@ case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: override def process(r: Row): Unit = { if (filter.evalBooleanFunction(r)) { - val ex = new GorDataException(s"Gor throw on: $filterSrc", -1) + // Nor rows carry the internal ChromNOR/PosNOR columns, show only the user columns + val (errorHeader, errorRow) = + if (executeNor) (header.split('\t').drop(2).mkString("\t"), r.otherCols()) + else (header, r.toString) + // doFormat=false: header and row are rendered from the exception fields, keep them out of the message + val ex = new GorDataException(errorMessage, -1, errorHeader, errorRow, null, false) if (isRetriable) { ex.fullRetry() } diff --git a/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala b/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala index bad08ee4..153307bf 100644 --- a/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala +++ b/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala @@ -23,18 +23,20 @@ package gorsat.Commands import gorsat.Analysis.ThrowIfAnalysis -import gorsat.Commands.CommandParseUtilities.hasOption +import gorsat.Commands.CommandParseUtilities.{hasOption, replaceSingleQuotes, stringValueOfOptionWithDefault} import org.gorpipe.gor.session.GorContext class ThrowIf extends CommandInfo("THROWIF", - CommandArguments("-retriable", "", 1, -1, ignoreIllegalArguments = true), + CommandArguments("-retriable", "-m", 1, -1, ignoreIllegalArguments = true), CommandOptions(gorCommand = true, norCommand = true)) { override def processArguments(context: GorContext, argString: String, iargs: Array[String], args: Array[String], executeNor: Boolean, forcedInputHeader: String) : CommandParsingResult = { - val pipeStep = ThrowIfAnalysis(context, executeNor, iargs.mkString(" "), forcedInputHeader, hasOption(args, "-retriable")) + val message = replaceSingleQuotes(stringValueOfOptionWithDefault(args, "-m", null)) + val pipeStep = ThrowIfAnalysis(context, executeNor, iargs.mkString(" "), forcedInputHeader, + hasOption(args, "-retriable"), message) CommandParsingResult(pipeStep, forcedInputHeader) } } \ No newline at end of file diff --git a/gortools/src/test/java/gorsat/UTestThrowIf.java b/gortools/src/test/java/gorsat/UTestThrowIf.java new file mode 100644 index 00000000..a7d97fbd --- /dev/null +++ b/gortools/src/test/java/gorsat/UTestThrowIf.java @@ -0,0 +1,95 @@ +/* + * BEGIN_COPYRIGHT + * + * Copyright (C) 2011-2013 deCODE genetics Inc. + * Copyright (C) 2013-2019 WuXi NextCode Inc. + * All Rights Reserved. + * + * GORpipe is free software: you can redistribute it and/or modify + * it under the terms of the AFFERO GNU General Public License as published by + * the Free Software Foundation. + * + * GORpipe is distributed "AS-IS" AND WITHOUT ANY WARRANTY OF ANY KIND, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE. See + * the AFFERO GNU General Public License for the complete license terms. + * + * You should have received a copy of the AFFERO GNU General Public License + * along with GORpipe. If not, see + * + * END_COPYRIGHT + */ + +package gorsat; + +import org.gorpipe.exceptions.ExceptionUtilities; +import org.gorpipe.exceptions.GorDataException; +import org.junit.Assert; +import org.junit.Test; + +public class UTestThrowIf { + + private static GorDataException runExpectingThrow(String query) { + try { + TestUtils.runGorPipe(query); + } catch (GorDataException e) { + return e; + } + Assert.fail("Expected GorDataException for: " + query); + return null; + } + + private static int occurrences(String text, String sub) { + return text.split(sub, -1).length - 1; + } + + @Test + public void defaultMessageUnchanged() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif status != 'mapped'"); + Assert.assertEquals("Gor throw on: status != 'mapped'", e.getMessage()); + Assert.assertFalse(e.isFullRetry()); + } + + @Test + public void customMessage() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m 'liftover failed' status != 'mapped'"); + Assert.assertEquals("liftover failed", e.getMessage()); + } + + @Test + public void exceptionCarriesOnlyOffendingRow() { + GorDataException e = runExpectingThrow("gorrows -p chr1:1-4 | calc status if(pos=2,'unmapped','mapped') | throwif status != 'mapped'"); + Assert.assertEquals("chrom\tpos\tstatus", e.getHeader()); + Assert.assertEquals("chr1\t2\tunmapped", e.getRow()); + } + + @Test + public void renderedErrorShowsHeaderAndRowOnce() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m 'liftover failed' status != 'mapped'"); + String rendered = ExceptionUtilities.gorExceptionToString(e); + Assert.assertTrue(rendered, rendered.contains("liftover failed\n\nHeader: chrom\tpos\tstatus\nRow: chr1\t1\tunmapped")); + Assert.assertEquals(rendered, 1, occurrences(rendered, "Header: ")); + Assert.assertEquals(rendered, 1, occurrences(rendered, "Row: ")); + } + + @Test + public void customMessageWithQuotes() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m \"it's 'not' mapped\" status != 'mapped'"); + Assert.assertEquals("it's 'not' mapped", e.getMessage()); + } + + @Test + public void customMessageWithRetriable() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -retriable -m 'liftover failed' status != 'mapped'"); + Assert.assertEquals("liftover failed", e.getMessage()); + Assert.assertTrue(e.isFullRetry()); + } + + @Test + public void norContext() { + GorDataException e = runExpectingThrow("norrows 3 | calc status if(rownum=1,'unmapped','mapped') | throwif -m 'liftover failed' status != 'mapped'"); + Assert.assertEquals("liftover failed", e.getMessage()); + Assert.assertEquals("RowNum\tstatus", e.getHeader()); + Assert.assertEquals("1\tunmapped", e.getRow()); + } +} diff --git a/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala b/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala index 4153618a..1f30e573 100644 --- a/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala +++ b/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala @@ -73,4 +73,28 @@ class UTestThrowIfAnalysis extends AnyFlatSpec { assert(thrown.getMessage == "Gor throw on: A=='ABC'") assert(thrown.isFullRetry() == true) } + + it should "use custom message when message is given" in { + val sink = AnalysisSink() + val header = "chrom\tpos\tA" + val pipe = ThrowIfAnalysis(context, executeNor = false, "A=='ABC'", header, message = "custom failure") | sink + pipe.setRowHeader(RowHeader(header, List("S", "I", "S").toArray)) + + val thrown = intercept[GorDataException](pipe.process(RowObj("chr1\t1\tABC"))) + assert(thrown.getMessage == "custom failure") + assert(thrown.getHeader == header) + assert(thrown.getRow == "chr1\t1\tABC") + } + + it should "combine custom message and retriable" in { + val sink = AnalysisSink() + val header = "chrom\tpos\tA" + val pipe = ThrowIfAnalysis(context, executeNor = false, "A=='ABC'", header, isRetriable = true, + message = "custom failure") | sink + pipe.setRowHeader(RowHeader(header, List("S", "I", "S").toArray)) + + val thrown = intercept[GorDataException](pipe.process(RowObj("chr1\t1\tABC"))) + assert(thrown.getMessage == "custom failure") + assert(thrown.isFullRetry()) + } }