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
42 changes: 21 additions & 21 deletions source/MRViewer/MRUITestEngineControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Expected<std::vector<PathedEntry>> listAllEntries( const std::vector<std::string
return ret;
}

Expected<void> pressButton( const std::vector<std::string>& path )
Expected<std::string> pressButton( const std::vector<std::string>& path )
{
if ( path.empty() )
return unexpected( "pressButton: Empty path not allowed here." );
Expand All @@ -177,7 +177,7 @@ Expected<void> pressButton( const std::vector<std::string>& path )

const std::string_view disabledReason = ( *buttonEx )->disabledReason;
if ( !disabledReason.empty() )
return unexpected( fmt::format( "pressButton {}: {}", pathToString( path ), composeStatus( disabledReason ) ) );
return composeStatus( disabledReason );

( *buttonEx )->simulateClick = true;

Expand Down Expand Up @@ -276,7 +276,7 @@ template Expected<Value<std::string >> readValue( const std::vector<std::string


template <typename T>
Expected<void> writeValue( const std::vector<std::string>& path, T value )
Expected<std::string> writeValue( const std::vector<std::string>& path, T value )
{
if ( path.empty() )
return unexpected( "writeValue: Empty path not allowed here." );
Expand All @@ -297,9 +297,9 @@ Expected<void> writeValue( const std::vector<std::string>& path, T value )

const std::string_view disabledReason = entry.disabledReason;
if ( !disabledReason.empty() )
return unexpected( fmt::format( "writeValue {}: {}", pathToString( path ), composeStatus( disabledReason ) ) );
return composeStatus( disabledReason );

auto writeValueOfCorrectType = [&entry, &path]( auto fixedValue ) -> Expected<void>
auto writeValueOfCorrectType = [&entry, &path]( auto fixedValue ) -> Expected<std::string>
{
using U = decltype( fixedValue );
auto &target = std::get<TestEngine::ValueEntry::Value<U>>( entry.value );
Expand Down Expand Up @@ -347,19 +347,19 @@ Expected<void> writeValue( const std::vector<std::string>& path, T value )
else if constexpr ( std::is_same_v<T, double> )
{
return std::visit( MR::overloaded{
[&]( const TestEngine::ValueEntry::Value<std::string >& ) -> Expected<void> { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<double >& ) -> Expected<void> { return writeValueOfCorrectType( value ); },
[&]( const TestEngine::ValueEntry::Value<std::int64_t >& ) -> Expected<void> { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<std::uint64_t>& ) -> Expected<void> { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<std::string >& ) -> Expected<std::string> { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<double >& ) -> Expected<std::string> { return writeValueOfCorrectType( value ); },
[&]( const TestEngine::ValueEntry::Value<std::int64_t >& ) -> Expected<std::string> { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<std::uint64_t>& ) -> Expected<std::string> { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); },
}, entry.value );
}
else if constexpr ( std::is_same_v<T, std::int64_t> )
{
return std::visit( MR::overloaded{
[&]( const TestEngine::ValueEntry::Value<std::string >& ) -> Expected<void> { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<double >& ) -> Expected<void> { return writeValueOfCorrectType( double( value ) ); },
[&]( const TestEngine::ValueEntry::Value<std::int64_t >& ) -> Expected<void> { return writeValueOfCorrectType( value ); },
[&]( const TestEngine::ValueEntry::Value<std::uint64_t>& ) -> Expected<void>
[&]( const TestEngine::ValueEntry::Value<std::string >& ) -> Expected<std::string> { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<double >& ) -> Expected<std::string> { return writeValueOfCorrectType( double( value ) ); },
[&]( const TestEngine::ValueEntry::Value<std::int64_t >& ) -> Expected<std::string> { return writeValueOfCorrectType( value ); },
[&]( const TestEngine::ValueEntry::Value<std::uint64_t>& ) -> Expected<std::string>
{
if ( value < 0 )
return unexpected( fmt::format( "writeValue: `{}` is unsigned, but received a negative number.", pathToString( path ) ) );
Expand All @@ -370,10 +370,10 @@ Expected<void> writeValue( const std::vector<std::string>& path, T value )
else if constexpr ( std::is_same_v<T, std::uint64_t> )
{
return std::visit( MR::overloaded{
[&]( const TestEngine::ValueEntry::Value<std::string >& ) -> Expected<void> { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<double >& ) -> Expected<void> { return writeValueOfCorrectType( double( value ) ); },
[&]( const TestEngine::ValueEntry::Value<std::uint64_t>& ) -> Expected<void> { return writeValueOfCorrectType( value ); },
[&]( const TestEngine::ValueEntry::Value<std::int64_t >& ) -> Expected<void>
[&]( const TestEngine::ValueEntry::Value<std::string >& ) -> Expected<std::string> { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); },
[&]( const TestEngine::ValueEntry::Value<double >& ) -> Expected<std::string> { return writeValueOfCorrectType( double( value ) ); },
[&]( const TestEngine::ValueEntry::Value<std::uint64_t>& ) -> Expected<std::string> { return writeValueOfCorrectType( value ); },
[&]( const TestEngine::ValueEntry::Value<std::int64_t >& ) -> Expected<std::string>
{
if ( value > std::uint64_t( std::numeric_limits<std::int64_t>::max() ) )
return unexpected( fmt::format( "writeValue: `{}` is signed, but received an unsigned integer large enough to not be representable as `int64_t`.", pathToString( path ) ) );
Expand All @@ -383,9 +383,9 @@ Expected<void> writeValue( const std::vector<std::string>& path, T value )
}
}

template Expected<void> writeValue( const std::vector<std::string>& path, std::int64_t value );
template Expected<void> writeValue( const std::vector<std::string>& path, std::uint64_t value );
template Expected<void> writeValue( const std::vector<std::string>& path, double value );
template Expected<void> writeValue( const std::vector<std::string>& path, std::string value );
template Expected<std::string> writeValue( const std::vector<std::string>& path, std::int64_t value );
template Expected<std::string> writeValue( const std::vector<std::string>& path, std::uint64_t value );
template Expected<std::string> writeValue( const std::vector<std::string>& path, double value );
template Expected<std::string> writeValue( const std::vector<std::string>& path, std::string value );

} // namespace MR::UI::TestEngine::Control
22 changes: 14 additions & 8 deletions source/MRViewer/MRUITestEngineControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ using PathedEntry = std::pair<std::vector<std::string>, TypedEntry>;
// and their descendants appear on subsequent rows with `path` extending theirs.
[[nodiscard]] MRVIEWER_API Expected<std::vector<PathedEntry>> listAllEntries( const std::vector<std::string>& rootPath );

// Presses the button at this path, or returns an error.
MRVIEWER_API Expected<void> pressButton( const std::vector<std::string>& path );
// Presses the button at this path.
// Returns empty string on success (click simulated). If the button was drawn disabled, the press is a silent
// no-op and the return is a non-empty status (`"disabled"` / `"disabled: <reason>"`) matching `composeStatus()`.
// `unexpected` is returned only for hard errors (path not found, entry is not a button).
MRVIEWER_API Expected<std::string> pressButton( const std::vector<std::string>& path );

// Read/write values: (drags, sliders, etc)

Expand Down Expand Up @@ -101,14 +104,17 @@ extern template MRVIEWER_API Expected<Value<std::uint64_t>> readValue( const std
extern template MRVIEWER_API Expected<Value<double >> readValue( const std::vector<std::string>& path );
extern template MRVIEWER_API Expected<Value<std::string >> readValue( const std::vector<std::string>& path );

// Modifies the value at the `path`, or returns an error if the path, type or value are wrong.
// Modifies the value at the `path`.
// Returns empty string on success (write simulated). If the widget was drawn disabled, the write is a silent
// no-op and the return is a non-empty status (`"disabled"` / `"disabled: <reason>"`) matching `composeStatus()`.
// `unexpected` is returned only for hard errors (path not found, wrong type, out-of-range / not-in-allowedValues).
template <typename T>
MRVIEWER_API Expected<void> writeValue( const std::vector<std::string>& path, T value );
MRVIEWER_API Expected<std::string> writeValue( const std::vector<std::string>& path, T value );

extern template MRVIEWER_API Expected<void> writeValue( const std::vector<std::string>& path, std::int64_t value );
extern template MRVIEWER_API Expected<void> writeValue( const std::vector<std::string>& path, std::uint64_t value );
extern template MRVIEWER_API Expected<void> writeValue( const std::vector<std::string>& path, double value );
extern template MRVIEWER_API Expected<void> writeValue( const std::vector<std::string>& path, std::string value );
extern template MRVIEWER_API Expected<std::string> writeValue( const std::vector<std::string>& path, std::int64_t value );
extern template MRVIEWER_API Expected<std::string> writeValue( const std::vector<std::string>& path, std::uint64_t value );
extern template MRVIEWER_API Expected<std::string> writeValue( const std::vector<std::string>& path, double value );
extern template MRVIEWER_API Expected<std::string> writeValue( const std::vector<std::string>& path, std::string value );


} // namespace MR::UI::TestEngine::Control
13 changes: 11 additions & 2 deletions source/MRViewer/MRViewerMcp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "MRMcp/MRMcp.h"
#include "MRMesh/MROnInit.h"
#include "MRPch/MRFmt.h"
#include "MRViewer/MRCommandLoop.h"
#include "MRViewer/MRUITestEngineControl.h"

Expand Down Expand Up @@ -75,11 +76,15 @@ static nlohmann::json mcpToolListAllUiEntries( const nlohmann::json& args )

static nlohmann::json mcpToolPressButton( const nlohmann::json& args )
{
const auto path = args.at( "path" ).get<std::vector<std::string>>();
MR::CommandLoop::runCommandFromGUIThread( [&]
{
auto ex = UI::TestEngine::Control::pressButton( args.at( "path" ).get<std::vector<std::string>>() );
auto ex = UI::TestEngine::Control::pressButton( path );
if ( !ex )
throw std::runtime_error( ex.error() );
// Non-empty = disabled status; surface to MCP as an error (empty = OK, click simulated).
if ( !ex->empty() )
throw std::runtime_error( fmt::format( "pressButton {}: {}", UI::TestEngine::Control::pathToString( path ), *ex ) );
} );
skipFramesAfterInput();

Expand Down Expand Up @@ -117,11 +122,15 @@ static nlohmann::json mcpToolReadValue( const nlohmann::json& args )
template <typename T>
static nlohmann::json mcpToolWriteValue( const nlohmann::json& args )
{
const auto path = args.at( "path" ).get<std::vector<std::string>>();
MR::CommandLoop::runCommandFromGUIThread( [&]
{
auto ex = UI::TestEngine::Control::writeValue<T>( args.at( "path" ).get<std::vector<std::string>>(), T( args.at( "value" ) ) );
auto ex = UI::TestEngine::Control::writeValue<T>( path, T( args.at( "value" ) ) );
if ( !ex )
throw std::runtime_error( ex.error() );
// Non-empty = disabled status; surface to MCP as an error (empty = OK, write simulated).
if ( !ex->empty() )
throw std::runtime_error( fmt::format( "writeValue {}: {}", UI::TestEngine::Control::pathToString( path ), *ex ) );
} );
skipFramesAfterInput();

Expand Down
10 changes: 8 additions & 2 deletions source/mrviewerpy/MRPythonUiInteraction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiPressButton,
MR::CommandLoop::runCommandFromGUIThread( [&]
{
spdlog::info( "pressButton {}: frame {}", MR::UI::TestEngine::Control::pathToString( path ), MR::getViewerInstance().getTotalFrames() );
MR::expectedValueOrThrow( MR::UI::TestEngine::Control::pressButton( path ) );
// Empty status = OK (click simulated); non-empty = disabled (silent no-op — pre-#5961 test contract).
auto status = MR::expectedValueOrThrow( MR::UI::TestEngine::Control::pressButton( path ) );
if ( !status.empty() )
spdlog::warn( "pressButton {}: {} (silent no-op)", MR::UI::TestEngine::Control::pathToString( path ), status );
} );
for ( int i = 0; i < MR::getViewerInstance().forceRedrawMinimumIncrementAfterEvents; ++i )
MR::CommandLoop::runCommandFromGUIThread( [] {} ); // Wait a few frames.
Expand Down Expand Up @@ -117,7 +120,10 @@ namespace
{
MR::CommandLoop::runCommandFromGUIThread( [&]
{
MR::expectedValueOrThrow( Control::writeValue<T>( path, std::move( value ) ) );
// Empty status = OK (write simulated); non-empty = disabled (silent no-op — pre-#5961 test contract).
auto status = MR::expectedValueOrThrow( Control::writeValue<T>( path, std::move( value ) ) );
if ( !status.empty() )
spdlog::warn( "writeValue {}: {} (silent no-op)", Control::pathToString( path ), status );
} );
}
}
Expand Down
Loading