Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion documentation/src/command/THROWIF.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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: <condition>``. Use ``-m`` to give a custom message. The error also
includes the header and the row that triggered the exception.

Usage
=====

Expand All @@ -21,4 +24,23 @@ Options

+--------------------+---------------------------------------------------------------------------------------+
| ``-retriable`` | If set an retriable exception is thrown. |
+--------------------+---------------------------------------------------------------------------------------+
+--------------------+---------------------------------------------------------------------------------------+
| ``-m <message>`` | Use ``<message>`` as the error message instead of ``Gor throw on: <condition>``. |
| | 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
18 changes: 16 additions & 2 deletions gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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: <condition>", 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
Expand All @@ -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()
}
Expand Down
8 changes: 5 additions & 3 deletions gortools/src/main/scala/gorsat/Commands/ThrowIf.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
95 changes: 95 additions & 0 deletions gortools/src/test/java/gorsat/UTestThrowIf.java
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/agpl-3.0.html>
*
* 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());
}
}
24 changes: 24 additions & 0 deletions gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
Loading