Files
ResInsight/ApplicationLibCode/UnitTests/RimWorkflowDescribeTools-Test.cpp
T

201 lines
11 KiB
C++
Raw Normal View History

#include "gtest/gtest.h"
#include "RiaTestDataDirectory.h"
#include "RimWorkflowDescribeTools.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace
{
// Output of `taskmaestro workflow describe workflow.yaml --input input.yaml --json` for a
// synthetic workflow: start -> greet_a, greet_b -> merge (collect), merge <- start.count
QJsonObject describeFixture()
{
QFile file( QString( "%1/RimWorkflowDescribeTools/describe.json" ).arg( TEST_DATA_DIR ) );
if ( !file.open( QIODevice::ReadOnly ) ) return {};
return QJsonDocument::fromJson( file.readAll() ).object();
}
QJsonObject taskByName( const QJsonObject& graph, const QString& name )
{
for ( const QJsonValue& task : graph.value( "tasks" ).toArray() )
{
if ( task.toObject().value( "name" ).toString() == name ) return task.toObject();
}
return {};
}
QJsonObject fieldByName( const QJsonObject& task, const QString& name )
{
for ( const QJsonValue& field : task.value( "config_fields" ).toArray() )
{
if ( field.toObject().value( "name" ).toString() == name ) return field.toObject();
}
return {};
}
} // namespace
//--------------------------------------------------------------------------------------------------
TEST( RimWorkflowDescribeTools, tasksInputsAndOutputs )
{
const QJsonObject describe = describeFixture();
ASSERT_FALSE( describe.isEmpty() );
const QJsonObject graph = RimWorkflowDescribeTools::graphFromDescribe( describe );
EXPECT_EQ( "synthetic", graph.value( "name" ).toString() );
QStringList names;
for ( const QJsonValue& task : graph.value( "tasks" ).toArray() )
names.append( task.toObject().value( "name" ).toString() );
EXPECT_EQ( QStringList( { "start", "greet_a", "greet_b", "merge" } ), names );
// The opaque ObjectModel `value` is not an output port
EXPECT_EQ( QJsonArray( { "count" } ), taskByName( graph, "start" ).value( "outputs" ).toArray() );
EXPECT_EQ( QJsonArray( { "message" } ), taskByName( graph, "greet_a" ).value( "outputs" ).toArray() );
EXPECT_EQ( QJsonArray( { "count", "messages" } ), taskByName( graph, "merge" ).value( "inputs" ).toArray() );
EXPECT_TRUE( taskByName( graph, "start" ).value( "config_fields" ).toArray().isEmpty() );
EXPECT_EQ( 9, taskByName( graph, "greet_a" ).value( "config_fields" ).toArray().size() );
}
//--------------------------------------------------------------------------------------------------
TEST( RimWorkflowDescribeTools, configFieldTypesAndValues )
{
const QJsonObject graph = RimWorkflowDescribeTools::graphFromDescribe( describeFixture() );
const QJsonObject greetA = taskByName( graph, "greet_a" );
const QJsonObject greetB = taskByName( graph, "greet_b" );
// input.yaml value wins over the model default
const QJsonObject name = fieldByName( greetA, "name" );
EXPECT_EQ( "string", name.value( "type" ).toString() );
EXPECT_EQ( "alice", name.value( "default" ).toString() );
EXPECT_EQ( "Person to greet", name.value( "description" ).toString() );
EXPECT_FALSE( name.value( "required" ).toBool() );
EXPECT_EQ( "world", fieldByName( greetB, "name" ).value( "default" ).toString() );
const QJsonObject times = fieldByName( greetA, "times" );
EXPECT_EQ( "integer", times.value( "type" ).toString() );
EXPECT_TRUE( times.value( "required" ).toBool() );
EXPECT_EQ( 3, times.value( "default" ).toInt() );
EXPECT_FALSE( fieldByName( greetB, "times" ).contains( "default" ) );
// Optional[int] is peeled; an explicit null in input.yaml is preserved
const QJsonObject optionalCount = fieldByName( greetA, "optional_count" );
EXPECT_EQ( "integer", optionalCount.value( "type" ).toString() );
EXPECT_TRUE( optionalCount.contains( "default" ) );
EXPECT_TRUE( optionalCount.value( "default" ).isNull() );
EXPECT_EQ( "array", fieldByName( greetA, "values" ).value( "type" ).toString() );
const QJsonObject when = fieldByName( greetA, "when" );
EXPECT_EQ( "date", when.value( "format" ).toString() );
EXPECT_EQ( "2024-01-01", when.value( "default" ).toString() );
EXPECT_EQ( "path", fieldByName( greetA, "out_file" ).value( "format" ).toString() );
EXPECT_EQ( "/tmp/hi.txt", fieldByName( greetA, "out_file" ).value( "default" ).toString() );
EXPECT_EQ( "directory-path", fieldByName( greetA, "out_dir" ).value( "format" ).toString() );
EXPECT_EQ( "/tmp", fieldByName( greetA, "out_dir" ).value( "default" ).toString() );
}
//--------------------------------------------------------------------------------------------------
TEST( RimWorkflowDescribeTools, resinsightObjectFields )
{
const QJsonObject graph = RimWorkflowDescribeTools::graphFromDescribe( describeFixture() );
const QJsonObject greetA = taskByName( graph, "greet_a" );
const QJsonObject wellPath = fieldByName( greetA, "well_path" );
EXPECT_EQ( "object", wellPath.value( "type" ).toString() );
EXPECT_EQ( "WellPath", wellPath.value( "resinsight_type" ).toString() );
EXPECT_EQ( "Well path to use", wellPath.value( "description" ).toString() );
EXPECT_EQ( "WellPath", wellPath.value( "default" ).toObject().value( "__resinsight_ref__" ).toString() );
EXPECT_EQ( "B-2H", wellPath.value( "default" ).toObject().value( "well_path_name" ).toString() );
const QJsonObject gridCase = fieldByName( greetA, "case" );
EXPECT_EQ( "EclipseCase", gridCase.value( "resinsight_type" ).toString() );
EXPECT_TRUE( gridCase.value( "required" ).toBool() );
EXPECT_FALSE( fieldByName( greetA, "name" ).contains( "resinsight_type" ) );
}
//--------------------------------------------------------------------------------------------------
TEST( RimWorkflowDescribeTools, dependencyEdges )
{
const QJsonObject graph = RimWorkflowDescribeTools::graphFromDescribe( describeFixture() );
const QJsonArray expected = {
QJsonObject{ { "from", "start" }, { "to", "greet_a" } },
QJsonObject{ { "from", "start" }, { "to", "greet_b" } },
QJsonObject{ { "from", "start" }, { "to", "merge" }, { "input", "count" }, { "output", "count" } },
QJsonObject{ { "from", "greet_a" }, { "to", "merge" }, { "input", "messages" }, { "output", "message" } },
QJsonObject{ { "from", "greet_b" }, { "to", "merge" }, { "input", "messages" }, { "output", "message" } },
};
EXPECT_EQ( expected, graph.value( "edges" ).toArray() );
}
//--------------------------------------------------------------------------------------------------
TEST( RimWorkflowDescribeTools, singleFieldAndKeyedCollectionEdges )
{
const QJsonObject keyedMembers{ { "a", QJsonObject{ { "task", "first" }, { "field", QJsonValue() } } },
{ "b", QJsonObject{ { "task", "second" }, { "field", "value" } } } };
const QJsonObject describe{
{ "workflow", "routing" },
{ "tasks",
QJsonArray{ QJsonObject{ { "name", "next" }, { "depends_on", QJsonObject{ { "task", "producer" }, { "field", "value" } } } },
QJsonObject{ { "name", "merge" },
{ "depends_on",
QJsonObject{ { "items",
QJsonObject{ { "collect",
QJsonObject{ { "kind", "keyed" }, { "members", keyedMembers } } } } } } } } } },
};
const QJsonArray expected = {
QJsonObject{ { "from", "producer" }, { "to", "next" }, { "output", "value" } },
QJsonObject{ { "from", "first" }, { "to", "merge" }, { "input", "items" } },
QJsonObject{ { "from", "second" }, { "to", "merge" }, { "input", "items" }, { "output", "value" } },
};
EXPECT_EQ( expected, RimWorkflowDescribeTools::graphFromDescribe( describe ).value( "edges" ).toArray() );
}
//--------------------------------------------------------------------------------------------------
TEST( RimWorkflowDescribeTools, missingConfigFieldIsReported )
{
const QJsonObject describe{ { "workflow", "broken" },
{ "tasks",
QJsonArray{ QJsonObject{ { "name", "task" },
{ "config_fields", QJsonArray{ "ghost" } },
{ "input_schema", QJsonObject{ { "properties", QJsonObject{} } } } } } } };
const QJsonObject field = fieldByName( taskByName( RimWorkflowDescribeTools::graphFromDescribe( describe ), "task" ), "ghost" );
EXPECT_EQ( "string", field.value( "type" ).toString() );
EXPECT_TRUE( field.value( "required" ).toBool() );
EXPECT_FALSE( field.value( "error" ).toString().isEmpty() );
}
//--------------------------------------------------------------------------------------------------
TEST( RimWorkflowDescribeTools, errorFromDescribe )
{
EXPECT_TRUE( RimWorkflowDescribeTools::errorFromDescribe( describeFixture() ).isEmpty() );
const QJsonObject invalid{ { "status", "invalid" },
{ "error",
QJsonObject{ { "message", "Workflow configuration is invalid" }, { "task", "greet" }, { "field", "times" } } } };
EXPECT_EQ( "Workflow configuration is invalid (task 'greet', field 'times')", RimWorkflowDescribeTools::errorFromDescribe( invalid ) );
const QJsonObject invalidWithoutDetails{ { "status", "invalid" },
{ "error", QJsonObject{ { "message", "Workflow configuration is invalid" } } } };
EXPECT_EQ( "Workflow configuration is invalid", RimWorkflowDescribeTools::errorFromDescribe( invalidWithoutDetails ) );
}
//--------------------------------------------------------------------------------------------------
TEST( RimWorkflowDescribeTools, resinsightTypeFromPythonType )
{
EXPECT_EQ( "EclipseCase", RimWorkflowDescribeTools::resinsightTypeFromPythonType( "rips.generated.generated_classes.Reservoir" ) );
EXPECT_EQ( "EclipseCase", RimWorkflowDescribeTools::resinsightTypeFromPythonType( "rips.case.Case" ) );
EXPECT_EQ( "WellPath", RimWorkflowDescribeTools::resinsightTypeFromPythonType( "rips.generated.generated_classes.WellPath" ) );
EXPECT_EQ( "View", RimWorkflowDescribeTools::resinsightTypeFromPythonType( "rips.generated.generated_classes.EclipseView" ) );
EXPECT_TRUE( RimWorkflowDescribeTools::resinsightTypeFromPythonType( "rips.instance.Instance" ).isEmpty() );
EXPECT_TRUE( RimWorkflowDescribeTools::resinsightTypeFromPythonType( "mypackage.WellPath" ).isEmpty() );
EXPECT_TRUE( RimWorkflowDescribeTools::resinsightTypeFromPythonType( "WellPath" ).isEmpty() );
}