From 9204f4772a654d3d28b54e2b4dec16a8c7b865ad Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Tue, 23 Jun 2026 16:37:01 +0200 Subject: [PATCH 1/4] maint: Introduce `ScopedContextChange` for tests Signed-off-by: Julien Jerphanion --- libmamba/tests/include/mambatests.hpp | 102 ++++++ .../src/core/test_prefix_interoperability.cpp | 24 +- libmamba/tests/src/core/test_repoquery.cpp | 7 +- .../test_sharded_repodata_integration.cpp | 306 +++++------------- .../tests/src/core/test_virtual_packages.cpp | 19 +- 5 files changed, 201 insertions(+), 257 deletions(-) diff --git a/libmamba/tests/include/mambatests.hpp b/libmamba/tests/include/mambatests.hpp index da6c87a5aa..b6ec0760e2 100644 --- a/libmamba/tests/include/mambatests.hpp +++ b/libmamba/tests/include/mambatests.hpp @@ -8,7 +8,10 @@ #define LIBMAMBATESTS_HPP #include +#include #include +#include +#include #include "mamba/core/context.hpp" #include "mamba/core/output.hpp" @@ -87,6 +90,105 @@ namespace mambatests void operator()(const mamba::util::environment_map& env); }; + // RAII helper for C++ tests that temporarily override fields on the shared Context + // singleton (see context()). Tests often need to point at a temp prefix, tweak channels, + // or flip feature flags; without restoration, later tests inherit stale state. + // + // Save the value of each touched field on first use and restore it when the guard is + // destroyed. Repeated calls to the same setter only change the live value — the original + // snapshot is always what gets restored. + // + // Example: + // auto& ctx = mambatests::context(); + // mambatests::ScopedContextChange context_change{ ctx }; + // context_change.set_channels({ "conda-forge" }).set_offline(false); + class ScopedContextChange + { + public: + + explicit ScopedContextChange(mamba::Context& ctx) + : m_ctx(ctx) + { + } + + ~ScopedContextChange() + { + for (auto it = m_restorers.rbegin(); it != m_restorers.rend(); ++it) + { + (*it)(); + } + } + + ScopedContextChange& set_target_prefix(const mamba::fs::u8path& prefix) + { + touch( + &mamba::Context::prefix_params, + [&](auto& params) { params.target_prefix = prefix; } + ); + return *this; + } + + ScopedContextChange& set_prefix_data_interoperability(bool value) + { + touch(&mamba::Context::prefix_data_interoperability, [&](auto& field) { field = value; }); + return *this; + } + + ScopedContextChange& set_channels(std::vector channels) + { + touch(&mamba::Context::channels, [&](auto& field) { field = std::move(channels); }); + return *this; + } + + ScopedContextChange& set_use_sharded_repodata(bool value) + { + touch(&mamba::Context::use_sharded_repodata, [&](auto& field) { field = value; }); + return *this; + } + + ScopedContextChange& set_offline(bool value) + { + touch(&mamba::Context::offline, [&](auto& field) { field = value; }); + return *this; + } + + ScopedContextChange& set_platform(std::string platform) + { + touch(&mamba::Context::platform, [&](auto& field) { field = std::move(platform); }); + return *this; + } + + template + ScopedContextChange& preserve(T mamba::Context::* member) + { + touch(member, [](auto&) {}); + return *this; + } + + ScopedContextChange(const ScopedContextChange&) = delete; + ScopedContextChange& operator=(const ScopedContextChange&) = delete; + ScopedContextChange(ScopedContextChange&&) = delete; + ScopedContextChange& operator=(ScopedContextChange&&) = delete; + + private: + + template + void touch(T mamba::Context::* member, F&& mutator) + { + auto& field = m_ctx.*member; + if (m_saved_fields.insert(static_cast(&field)).second) + { + const T initial = field; + m_restorers.push_back([&field, initial] { field = initial; }); + } + mutator(field); + } + + mamba::Context& m_ctx; + std::vector> m_restorers; + std::unordered_set m_saved_fields; + }; + /****************************************** * Implementation of EnvironmentCleaner * ******************************************/ diff --git a/libmamba/tests/src/core/test_prefix_interoperability.cpp b/libmamba/tests/src/core/test_prefix_interoperability.cpp index 74115b7e68..914f1c0d24 100644 --- a/libmamba/tests/src/core/test_prefix_interoperability.cpp +++ b/libmamba/tests/src/core/test_prefix_interoperability.cpp @@ -190,7 +190,8 @@ namespace mamba fs::create_directories(prefix_path); auto& ctx = mambatests::context(); - const auto original_prefix_data_interoperability = ctx.prefix_data_interoperability; + mambatests::ScopedContextChange context_change{ ctx }; + auto channel_context = ChannelContext::make_simple(ctx); // Create a minimal conda environment @@ -234,7 +235,7 @@ namespace mamba SECTION("Pip packages are NOT included when prefix interoperability is disabled") { - ctx.prefix_data_interoperability = false; + context_change.set_prefix_data_interoperability(false); auto db = solver::libsolv::Database(channel_context.params()); load_installed_packages_in_database(ctx, db, prefix_data); @@ -255,7 +256,7 @@ namespace mamba SECTION("Pip packages ARE included when prefix interoperability is enabled") { - ctx.prefix_data_interoperability = true; + context_change.set_prefix_data_interoperability(true); auto db = solver::libsolv::Database(channel_context.params()); load_installed_packages_in_database(ctx, db, prefix_data); @@ -277,7 +278,7 @@ namespace mamba SECTION("Pip packages with conda equivalents are NOT added") { - ctx.prefix_data_interoperability = true; + context_change.set_prefix_data_interoperability(true); // Add a conda package with the same name as the pip package auto conda_boto3 = specs::PackageInfo("boto3", "1.13.21", "py310h12345_0", "conda-forge"); @@ -317,7 +318,7 @@ namespace mamba SECTION("Multiple pip packages are included when prefix interoperability is enabled") { - ctx.prefix_data_interoperability = true; + context_change.set_prefix_data_interoperability(true); // Add multiple pip packages auto pip_pkg1 = specs::PackageInfo("requests", "2.28.0", "pypi_0", "pypi"); @@ -364,9 +365,6 @@ namespace mamba REQUIRE(found_boto3); REQUIRE(pip_pkg_count >= 3); // At least our 3 pip packages } - - // Restore original value - ctx.prefix_data_interoperability = original_prefix_data_interoperability; } TEST_CASE("Transaction: pip package removal", "[core][prefix-interop]") @@ -403,7 +401,8 @@ namespace mamba fs::create_directories(prefix_path); auto& ctx = mambatests::context(); - const auto original_prefix_data_interoperability = ctx.prefix_data_interoperability; + mambatests::ScopedContextChange context_change{ ctx }; + auto channel_context = ChannelContext::make_simple(ctx); // Create a minimal conda environment @@ -436,7 +435,7 @@ namespace mamba SECTION("Full workflow: pip package detected and can be removed") { - ctx.prefix_data_interoperability = true; + context_change.set_prefix_data_interoperability(true); // Use no_pip=true to avoid trying to run pip inspect auto prefix_data = PrefixData::create(prefix_path, channel_context, true).value(); @@ -474,7 +473,7 @@ namespace mamba SECTION("Pip package exclusion when conda package exists") { - ctx.prefix_data_interoperability = true; + context_change.set_prefix_data_interoperability(true); // Use no_pip=true to avoid trying to run pip inspect auto prefix_data = PrefixData::create(prefix_path, channel_context, true).value(); @@ -518,9 +517,6 @@ namespace mamba REQUIRE_FALSE(found_pip); REQUIRE(boto3_count == 1); } - - // Restore original value - ctx.prefix_data_interoperability = original_prefix_data_interoperability; } } // namespace mamba diff --git a/libmamba/tests/src/core/test_repoquery.cpp b/libmamba/tests/src/core/test_repoquery.cpp index 032f9cd3fc..dc673d3ed6 100644 --- a/libmamba/tests/src/core/test_repoquery.cpp +++ b/libmamba/tests/src/core/test_repoquery.cpp @@ -14,7 +14,6 @@ #include "mamba/core/context.hpp" #include "mamba/core/history.hpp" #include "mamba/core/util.hpp" -#include "mamba/core/util_scope.hpp" #include "mambatests.hpp" @@ -137,14 +136,12 @@ TEST_CASE( ) { auto& ctx = mambatests::context(); - const auto saved_target_prefix = ctx.prefix_params.target_prefix; - on_scope_exit restore_target_prefix{ [&] - { ctx.prefix_params.target_prefix = saved_target_prefix; } }; + mambatests::ScopedContextChange context_change{ ctx }; const TemporaryDirectory tmp_dir; const fs::u8path conda_meta = tmp_dir.path() / "conda-meta"; fs::create_directories(conda_meta); - ctx.prefix_params.target_prefix = tmp_dir.path(); + context_change.set_target_prefix(tmp_dir.path()); ChannelContext channel_context = ChannelContext::make_conda_compatible(ctx); History history(tmp_dir.path(), channel_context); diff --git a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp index ed91628b5b..ee7a694a86 100644 --- a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp +++ b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp @@ -215,13 +215,9 @@ namespace const fs::u8path& cache_path ) { - // Save original shard setting - const bool original_use_shards = ctx.use_sharded_repodata; - - // Set shard usage - ctx.use_sharded_repodata = use_shards; - - on_scope_exit restore_settings{ [&] { ctx.use_sharded_repodata = original_use_shards; } }; + // Save original shard setting and restore when leaving scope. + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_use_sharded_repodata(use_shards); // Create database libsolv::Database db{ @@ -497,19 +493,10 @@ TEST_CASE( ) { auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }) + .set_use_sharded_repodata(true) + .set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -580,19 +567,10 @@ TEST_CASE( ) { auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }) + .set_use_sharded_repodata(true) + .set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -630,19 +608,8 @@ TEST_CASE( ) { auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { "conda-forge" }; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "conda-forge" }).set_use_sharded_repodata(true).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -674,25 +641,17 @@ TEST_CASE( TEST_CASE("Sharded repodata - solve xeus-python-dev specs on emscripten", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const std::string saved_platform = ctx.platform; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.platform = saved_platform; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { - "https://prefix.dev/emscripten-forge-4x", - "https://prefix.dev/conda-forge", - }; - ctx.platform = "emscripten-wasm32"; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change + .set_channels( + { + "https://prefix.dev/emscripten-forge-4x", + "https://prefix.dev/conda-forge", + } + ) + .set_platform("emscripten-wasm32") + .set_use_sharded_repodata(true) + .set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -734,25 +693,17 @@ TEST_CASE( { // Regression: cross-channel shard root expansion (noarch deps on conda-forge). auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const std::string saved_platform = ctx.platform; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.platform = saved_platform; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { - "https://prefix.dev/emscripten-forge-dev", - "conda-forge", - }; - ctx.platform = "emscripten-wasm32"; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change + .set_channels( + { + "https://prefix.dev/emscripten-forge-dev", + "conda-forge", + } + ) + .set_platform("emscripten-wasm32") + .set_use_sharded_repodata(true) + .set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -823,26 +774,18 @@ TEST_CASE( } auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const std::string saved_platform = ctx.platform; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.platform = saved_platform; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { - "file://" + flat_channel_dir.path().string(), - "https://prefix.dev/emscripten-forge-4x", - "conda-forge", - }; - ctx.platform = "emscripten-wasm32"; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change + .set_channels( + { + "file://" + flat_channel_dir.path().string(), + "https://prefix.dev/emscripten-forge-4x", + "conda-forge", + } + ) + .set_platform("emscripten-wasm32") + .set_use_sharded_repodata(true) + .set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -870,25 +813,17 @@ TEST_CASE( TEST_CASE("Sharded repodata - solve pyjs-obspy env specs on emscripten", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const std::string saved_platform = ctx.platform; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.platform = saved_platform; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { - "https://repo.prefix.dev/emscripten-forge-4x", - "https://repo.prefix.dev/conda-forge", - }; - ctx.platform = "emscripten-wasm32"; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change + .set_channels( + { + "https://repo.prefix.dev/emscripten-forge-4x", + "https://repo.prefix.dev/conda-forge", + } + ) + .set_platform("emscripten-wasm32") + .set_use_sharded_repodata(true) + .set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -929,19 +864,10 @@ TEST_CASE("Sharded repodata - solve omni env specs", "[mamba::core][sharded][.in { // Non-regression for https://github.com/mamba-org/mamba/issues/4277 auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { "conda-forge", "bioconda" }; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "conda-forge", "bioconda" }) + .set_use_sharded_repodata(true) + .set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -980,19 +906,8 @@ TEST_CASE("Sharded repodata - solve Apache Arrow sphinx env specs", "[mamba::cor // Non-regression for Apache Arrow docs environment: // https://raw.githubusercontent.com/apache/arrow/eb375a5c38b46ce96725f2f7f6376eba0e516e4f/ci/conda_env_sphinx.txt auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { "conda-forge" }; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "conda-forge" }).set_use_sharded_repodata(true).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1024,19 +939,8 @@ TEST_CASE("Sharded repodata - solve issue 4274 env specs", "[mamba::core][sharde { // Non-regression for https://github.com/mamba-org/mamba/issues/4274#issue-4437049295 auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - } }; - - ctx.channels = { "conda-forge" }; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "conda-forge" }).set_use_sharded_repodata(true).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1067,30 +971,20 @@ TEST_CASE("Sharded repodata - minrk gist downgrade non-regression", "[mamba::cor // Non-regression for https://gist.github.com/minrk/5fdabeb7ab8cd2c69cbc27588fed1903 // Flow: create from @EXPLICIT lock, then solve downgrade update specs. auto& ctx = mambatests::context(); - const std::vector saved_channels = ctx.channels; - const bool saved_use_shards = ctx.use_sharded_repodata; - const bool saved_offline = ctx.offline; - const auto saved_target_prefix = ctx.prefix_params.target_prefix; - on_scope_exit restore_ctx{ [&] - { - ctx.channels = saved_channels; - ctx.use_sharded_repodata = saved_use_shards; - ctx.offline = saved_offline; - ctx.prefix_params.target_prefix = saved_target_prefix; - } }; - - ctx.channels = { "conda-forge" }; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; const fs::u8path prefix_path = tmp_dir.path() / "minrk_env"; fs::create_directories(cache_dir); + context_change.set_channels({ "conda-forge" }) + .set_use_sharded_repodata(true) + .set_offline(false) + .set_target_prefix(prefix_path); + auto channel_context = ChannelContext::make_conda_compatible(ctx); init_channels(ctx, channel_context); - ctx.prefix_params.target_prefix = prefix_path; const auto explicit_urls = read_explicit_urls( mambatests::test_data_dir / "env_file/minrk_environment.py-3.9-linux-64.lock" @@ -1116,16 +1010,8 @@ TEST_CASE("Sharded repodata - minrk gist downgrade non-regression", "[mamba::cor auto solve_update_from_existing_prefix = [&](bool use_shards) -> std::pair { - const bool saved_mode = ctx.use_sharded_repodata; - const auto saved_prefix = ctx.prefix_params.target_prefix; - on_scope_exit restore_mode{ [&] - { - ctx.use_sharded_repodata = saved_mode; - ctx.prefix_params.target_prefix = saved_prefix; - } }; - - ctx.use_sharded_repodata = use_shards; - ctx.prefix_params.target_prefix = prefix_path; + mambatests::ScopedContextChange solve_context_change{ ctx }; + solve_context_change.set_use_sharded_repodata(use_shards).set_target_prefix(prefix_path); const auto started_at = std::chrono::steady_clock::now(); auto [db, package_caches] = prepare_solver_context( @@ -1402,13 +1288,9 @@ TEST_CASE("Sharded repodata - update scenarios", "[mamba::core][sharded][.integr std::vector update_specs = { "python" }; // For update, we need to create prefix data and use Update request - const bool original_use_shards = ctx.use_sharded_repodata; - const auto saved_safety_checks = ctx.validation_params.safety_checks; - on_scope_exit restore_ctx_update{ [&] - { - ctx.use_sharded_repodata = original_use_shards; - ctx.validation_params.safety_checks = saved_safety_checks; - } }; + mambatests::ScopedContextChange update_context_change{ ctx }; + update_context_change.preserve(&mamba::Context::use_sharded_repodata) + .preserve(&mamba::Context::validation_params); // Test traditional update ctx.use_sharded_repodata = false; @@ -1518,16 +1400,8 @@ TEST_CASE("Sharded repodata - update all uses history-expanded roots", "[mamba:: auto solve_update_all_like_api = [&](bool use_shards) -> UpdateAllSolveResult { - const bool saved_use_shards = ctx.use_sharded_repodata; - const auto saved_target_prefix = ctx.prefix_params.target_prefix; - on_scope_exit restore_ctx{ [&] - { - ctx.use_sharded_repodata = saved_use_shards; - ctx.prefix_params.target_prefix = saved_target_prefix; - } }; - - ctx.use_sharded_repodata = use_shards; - ctx.prefix_params.target_prefix = prefix_path; + mambatests::ScopedContextChange solve_context_change{ ctx }; + solve_context_change.set_use_sharded_repodata(use_shards).set_target_prefix(prefix_path); auto [db, package_caches] = prepare_solver_context( ctx, @@ -1625,16 +1499,8 @@ TEST_CASE("Sharded repodata - issue 4240 update-all example parity", "[mamba::co auto solve_update_all_like_api = [&](bool use_shards) -> UpdateAllSolveResult { - const bool saved_use_shards = ctx.use_sharded_repodata; - const auto saved_target_prefix = ctx.prefix_params.target_prefix; - on_scope_exit restore_ctx{ [&] - { - ctx.use_sharded_repodata = saved_use_shards; - ctx.prefix_params.target_prefix = saved_target_prefix; - } }; - - ctx.use_sharded_repodata = use_shards; - ctx.prefix_params.target_prefix = prefix_path; + mambatests::ScopedContextChange solve_context_change{ ctx }; + solve_context_change.set_use_sharded_repodata(use_shards).set_target_prefix(prefix_path); auto [db, package_caches] = prepare_solver_context( ctx, @@ -1747,13 +1613,9 @@ TEST_CASE("Sharded repodata - remove scenarios", "[mamba::core][sharded][.integr std::vector remove_specs = { "numpy" }; // For remove, we need to create prefix data and use Remove request - const bool original_use_shards = ctx.use_sharded_repodata; - const auto saved_safety_checks = ctx.validation_params.safety_checks; - on_scope_exit restore_ctx_remove{ [&] - { - ctx.use_sharded_repodata = original_use_shards; - ctx.validation_params.safety_checks = saved_safety_checks; - } }; + mambatests::ScopedContextChange remove_context_change{ ctx }; + remove_context_change.preserve(&mamba::Context::use_sharded_repodata) + .preserve(&mamba::Context::validation_params); // Test traditional remove ctx.use_sharded_repodata = false; diff --git a/libmamba/tests/src/core/test_virtual_packages.cpp b/libmamba/tests/src/core/test_virtual_packages.cpp index 292c02d701..bdef9a029a 100644 --- a/libmamba/tests/src/core/test_virtual_packages.cpp +++ b/libmamba/tests/src/core/test_virtual_packages.cpp @@ -19,17 +19,6 @@ namespace mamba namespace testing { - template - struct Finally - { - Callback callback; - - ~Finally() - { - callback(); - } - }; - namespace { TEST_CASE("make_virtual_package") @@ -52,6 +41,9 @@ namespace mamba using Version = specs::Version; auto& ctx = mambatests::context(); + mambatests::ScopedContextChange context_change{ ctx }; + context_change.preserve(&mamba::Context::platform); + auto pkgs = detail::dist_packages(ctx.platform); if (util::on_win) @@ -81,11 +73,6 @@ namespace mamba REQUIRE(pkgs.back().build_string.find("x86_64") == 0); #endif - // This is bad design, tests should not interfere - // Will get rid of that when implementing context as not a singleton - auto restore_ctx = [&ctx, old_plat = ctx.platform]() { ctx.platform = old_plat; }; - auto finally = Finally{ restore_ctx }; - util::set_env("CONDA_OVERRIDE_OSX", "12.1"); pkgs = detail::dist_packages("osx-arm"); REQUIRE(pkgs.size() == 3); From 7e9746fed24399da1482bc45b2e9d8d1ad37489c Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Tue, 23 Jun 2026 17:03:48 +0200 Subject: [PATCH 2/4] Cover other tests with `ScopedContextChange` Signed-off-by: Julien Jerphanion --- libmamba/tests/include/mambatests.hpp | 18 +++++++ .../tests/src/core/test_channel_loader.cpp | 50 ++++--------------- .../tests/src/core/test_channels_hook.cpp | 5 +- .../tests/src/core/test_configuration.cpp | 20 +++----- libmamba/tests/src/core/test_cpp.cpp | 5 +- libmamba/tests/src/core/test_env_lockfile.cpp | 7 ++- libmamba/tests/src/core/test_output.cpp | 8 ++- .../test_sharded_repodata_integration.cpp | 49 +++++++++--------- .../tests/src/core/test_virtual_packages.cpp | 2 - 9 files changed, 72 insertions(+), 92 deletions(-) diff --git a/libmamba/tests/include/mambatests.hpp b/libmamba/tests/include/mambatests.hpp index b6ec0760e2..4bcd21868d 100644 --- a/libmamba/tests/include/mambatests.hpp +++ b/libmamba/tests/include/mambatests.hpp @@ -128,6 +128,24 @@ namespace mambatests return *this; } + ScopedContextChange& set_root_prefix(const mamba::fs::u8path& prefix) + { + touch(&mamba::Context::prefix_params, [&](auto& params) { params.root_prefix = prefix; }); + return *this; + } + + ScopedContextChange& set_envs_dirs(std::vector dirs) + { + touch(&mamba::Context::envs_dirs, [&](auto& field) { field = std::move(dirs); }); + return *this; + } + + ScopedContextChange& set_pkgs_dirs(std::vector dirs) + { + touch(&mamba::Context::pkgs_dirs, [&](auto& field) { field = std::move(dirs); }); + return *this; + } + ScopedContextChange& set_prefix_data_interoperability(bool value) { touch(&mamba::Context::prefix_data_interoperability, [&](auto& field) { field = value; }); diff --git a/libmamba/tests/src/core/test_channel_loader.cpp b/libmamba/tests/src/core/test_channel_loader.cpp index 63fae24fe4..a623d24307 100644 --- a/libmamba/tests/src/core/test_channel_loader.cpp +++ b/libmamba/tests/src/core/test_channel_loader.cpp @@ -161,45 +161,13 @@ TEST_CASE("load_channels", "[mamba::api][channel_loader]") { // Use test singletons so Console/progress bar are initialized (avoids SIGABRT) Context& ctx = mambatests::context(); - - // Save and restore context so we don't affect other tests (e.g. test_configuration - // expects default ssl_verify / config state) - struct ContextGuard - { - Context& ctx; - std::vector channels; - std::map> mirrored_channels; - std::vector pkgs_dirs; - bool offline; - std::string ssl_verify; - std::string channel_alias; - std::map proxy_servers; - - explicit ContextGuard(Context& c) - : ctx(c) - , channels(c.channels) - , mirrored_channels(c.mirrored_channels) - , pkgs_dirs(c.pkgs_dirs) - , offline(c.offline) - , ssl_verify(c.remote_fetch_params.ssl_verify) - , channel_alias(c.channel_alias) - , proxy_servers(c.remote_fetch_params.proxy_servers) - { - } - - ~ContextGuard() - { - ctx.channels = std::move(channels); - ctx.mirrored_channels = std::move(mirrored_channels); - ctx.pkgs_dirs = std::move(pkgs_dirs); - ctx.offline = offline; - ctx.remote_fetch_params.ssl_verify = std::move(ssl_verify); - ctx.channel_alias = std::move(channel_alias); - ctx.remote_fetch_params.proxy_servers = std::move(proxy_servers); - } - }; - - ContextGuard guard(ctx); + mambatests::ScopedContextChange context_change{ ctx }; + context_change.preserve(&mamba::Context::channels) + .preserve(&mamba::Context::mirrored_channels) + .preserve(&mamba::Context::pkgs_dirs) + .preserve(&mamba::Context::offline) + .preserve(&mamba::Context::remote_fetch_params) + .preserve(&mamba::Context::channel_alias); ctx.channels = {}; ctx.mirrored_channels = {}; @@ -220,8 +188,8 @@ TEST_CASE("load_channels", "[mamba::api][channel_loader]") TEST_CASE("load_channels with root_packages", "[mamba::core][mamba::api::channel_loader]") { auto& ctx = mambatests::context(); - ctx.channels = { "conda-forge" }; - ctx.use_sharded_repodata = true; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "conda-forge" }).set_use_sharded_repodata(true); auto channel_context = ChannelContext::make_conda_compatible(ctx); solver::libsolv::Database db{ channel_context.params() }; diff --git a/libmamba/tests/src/core/test_channels_hook.cpp b/libmamba/tests/src/core/test_channels_hook.cpp index d88597c88c..9ab80763fb 100644 --- a/libmamba/tests/src/core/test_channels_hook.cpp +++ b/libmamba/tests/src/core/test_channels_hook.cpp @@ -43,13 +43,12 @@ namespace mamba::testing ChannelsHookFixture() { - m_channel_alias_bu = ctx.channel_alias; + m_context_change.preserve(&mamba::Context::channel_alias).preserve(&mamba::Context::channels); } ~ChannelsHookFixture() { config.reset_configurables(); - ctx.channel_alias = m_channel_alias_bu; mamba::util::unset_env("CONDA_CHANNELS"); } @@ -104,12 +103,12 @@ namespace mamba::testing mamba::Context& ctx = mambatests::context(); mamba::Configuration config{ ctx }; + mambatests::ScopedContextChange m_context_change{ ctx }; private: std::unique_ptr tempfile_ptr = std::make_unique("mambarc", ".yaml"); std::unique_ptr envfile_ptr = std::make_unique("env", ".yaml"); - std::string m_channel_alias_bu; }; } // namespace mamba::testing diff --git a/libmamba/tests/src/core/test_configuration.cpp b/libmamba/tests/src/core/test_configuration.cpp index 4307981148..d1f63168d5 100644 --- a/libmamba/tests/src/core/test_configuration.cpp +++ b/libmamba/tests/src/core/test_configuration.cpp @@ -39,17 +39,13 @@ namespace mamba Configuration() { - m_channel_alias_bu = ctx.channel_alias; - m_ssl_verify = ctx.remote_fetch_params.ssl_verify; - m_proxy_servers = ctx.remote_fetch_params.proxy_servers; + m_context_change.preserve(&mamba::Context::channel_alias) + .preserve(&mamba::Context::remote_fetch_params); } ~Configuration() { config.reset_configurables(); - ctx.channel_alias = m_channel_alias_bu; - ctx.remote_fetch_params.ssl_verify = m_ssl_verify; - ctx.remote_fetch_params.proxy_servers = m_proxy_servers; } protected: @@ -131,16 +127,10 @@ namespace mamba mamba::Context& ctx = mambatests::context(); mamba::Configuration config{ ctx }; + mambatests::ScopedContextChange m_context_change{ ctx }; private: - // Variables to restore the original Context state and avoid - // side effect across the tests. A better solution would be to - // save and restore the whole context (that requires refactoring - // of the Context class) - std::string m_channel_alias_bu; - std::string m_ssl_verify; - std::map m_proxy_servers; mambatests::EnvironmentCleaner restore = { mambatests::CleanMambaEnv() }; }; @@ -874,6 +864,9 @@ namespace mamba TEST_CASE_METHOD(Configuration, "platform") { + mambatests::ScopedContextChange context_change{ ctx }; + context_change.preserve(&mamba::Context::platform); + REQUIRE(ctx.platform == ctx.host_platform); std::string rc = "platform: mylinux-128"; @@ -903,7 +896,6 @@ namespace mamba ); config.at("platform").clear_values(); - ctx.platform = ctx.host_platform; } #define TEST_BOOL_CONFIGURABLE(NAME, CTX) \ diff --git a/libmamba/tests/src/core/test_cpp.cpp b/libmamba/tests/src/core/test_cpp.cpp index 4a4c3e4789..26e24d3edb 100644 --- a/libmamba/tests/src/core/test_cpp.cpp +++ b/libmamba/tests/src/core/test_cpp.cpp @@ -368,8 +368,9 @@ namespace mamba if constexpr (util::on_mac || util::on_linux) { auto& ctx = mambatests::context(); - ctx.prefix_params.root_prefix = "/home/user/micromamba/"; - ctx.envs_dirs = { ctx.prefix_params.root_prefix / "envs" }; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_root_prefix("/home/user/micromamba/") + .set_envs_dirs({ fs::u8path("/home/user/micromamba/envs") }); fs::u8path prefix = "/home/user/micromamba/envs/testprefix"; auto& pp = ctx.prefix_params; diff --git a/libmamba/tests/src/core/test_env_lockfile.cpp b/libmamba/tests/src/core/test_env_lockfile.cpp index 2b15400edf..b8db3d6e5b 100644 --- a/libmamba/tests/src/core/test_env_lockfile.cpp +++ b/libmamba/tests/src/core/test_env_lockfile.cpp @@ -412,6 +412,9 @@ namespace mamba // so we only have this test for yaml/conda env-lock-files. auto& ctx = mambatests::context(); + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_platform("linux-64"); + const fs::u8path lockfile_path{ mambatests::test_data_dir / "env_lockfile/good_multiple_categories-lock.yaml" }; auto channel_context = ChannelContext::make_conda_compatible(mambatests::context()); @@ -419,8 +422,6 @@ namespace mamba add_logger_to_database(db); mamba::MultiPackageCache pkg_cache({ "/tmp/" }, ctx.validation_params); - ctx.platform = "linux-64"; - auto check_categories = [&](std::vector categories, size_t num_conda, size_t num_pip) { @@ -450,8 +451,6 @@ namespace mamba check_categories({ "main", "dev" }, 31, 6); check_categories({ "dev" }, 28, 1); check_categories({ "nonesuch" }, 0, 0); - - ctx.platform = ctx.host_platform; } } diff --git a/libmamba/tests/src/core/test_output.cpp b/libmamba/tests/src/core/test_output.cpp index 1ef7827e24..e04f0e509c 100644 --- a/libmamba/tests/src/core/test_output.cpp +++ b/libmamba/tests/src/core/test_output.cpp @@ -17,12 +17,16 @@ namespace mamba { TEST_CASE("no_progress_bars") { - mambatests::context().graphics_params.no_progress_bars = true; + auto& ctx = mambatests::context(); + mambatests::ScopedContextChange context_change{ ctx }; + context_change.preserve(&mamba::Context::graphics_params); + + ctx.graphics_params.no_progress_bars = true; auto proxy = Console::instance().add_progress_bar("conda-forge"); REQUIRE_FALSE(proxy.defined()); REQUIRE_FALSE(proxy); - mambatests::context().graphics_params.no_progress_bars = false; + ctx.graphics_params.no_progress_bars = false; proxy = Console::instance().add_progress_bar("conda-forge"); REQUIRE(proxy.defined()); REQUIRE(proxy); diff --git a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp index ee7a694a86..67b517e9ff 100644 --- a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp +++ b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp @@ -454,14 +454,15 @@ namespace TEST_CASE("Sharded repodata - load_channels accepts root_packages", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.use_sharded_repodata = true; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }) + .set_use_sharded_repodata(true) + .set_offline(false); // Use a temp directory for package cache to ensure a writable path (required for shard index // and shard caching in CI environments where default pkgs_dirs may not be writable) const auto tmp_dir = TemporaryDirectory(); - ctx.pkgs_dirs = { tmp_dir.path() / "pkgs" }; + context_change.set_pkgs_dirs({ tmp_dir.path() / "pkgs" }); create_cache_dir(ctx.pkgs_dirs.front()); auto channel_context = ChannelContext::make_conda_compatible(ctx); @@ -524,8 +525,8 @@ TEST_CASE( TEST_CASE("Sharded repodata - noarch-only root package is installable", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1069,8 +1070,8 @@ TEST_CASE("Sharded repodata - minrk gist downgrade non-regression", "[mamba::cor TEST_CASE("Sharded repodata - solver results consistency", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1143,8 +1144,8 @@ TEST_CASE("Sharded repodata - solver results consistency", "[mamba::core][sharde TEST_CASE("Sharded repodata - environment consistency", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1193,8 +1194,8 @@ TEST_CASE("Sharded repodata - environment consistency", "[mamba::core][sharded][ TEST_CASE("Sharded repodata - cross-subdir dependencies", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1249,8 +1250,8 @@ TEST_CASE("Sharded repodata - cross-subdir dependencies", "[mamba::core][sharded TEST_CASE("Sharded repodata - update scenarios", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1371,8 +1372,8 @@ TEST_CASE("Sharded repodata - update scenarios", "[mamba::core][sharded][.integr TEST_CASE("Sharded repodata - update all uses history-expanded roots", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1461,8 +1462,8 @@ TEST_CASE("Sharded repodata - update all uses history-expanded roots", "[mamba:: TEST_CASE("Sharded repodata - issue 4240 update-all example parity", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1574,8 +1575,8 @@ TEST_CASE("Sharded repodata - issue 4240 update-all example parity", "[mamba::co TEST_CASE("Sharded repodata - remove scenarios", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const TemporaryDirectory tmp_dir; const fs::u8path cache_dir = tmp_dir.path() / "cache"; @@ -1720,8 +1721,8 @@ TEST_CASE( ) { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const auto tmp_dir = TemporaryDirectory(); const auto cache_dir = tmp_dir.path() / "cache"; @@ -1772,8 +1773,8 @@ TEST_CASE( TEST_CASE("Sharded repodata - libblas implementation preference", "[mamba::core][sharded][.integration]") { auto& ctx = mambatests::context(); - ctx.channels = { "https://prefix.dev/conda-forge" }; - ctx.offline = false; + mambatests::ScopedContextChange context_change{ ctx }; + context_change.set_channels({ "https://prefix.dev/conda-forge" }).set_offline(false); const auto tmp_dir = TemporaryDirectory(); const auto cache_dir = tmp_dir.path() / "cache"; diff --git a/libmamba/tests/src/core/test_virtual_packages.cpp b/libmamba/tests/src/core/test_virtual_packages.cpp index bdef9a029a..cee2483f35 100644 --- a/libmamba/tests/src/core/test_virtual_packages.cpp +++ b/libmamba/tests/src/core/test_virtual_packages.cpp @@ -112,8 +112,6 @@ namespace mamba REQUIRE(pkgs[0].name == "__unix"); REQUIRE(pkgs[1].name == "__archspec"); REQUIRE(pkgs[1].build_string == "wasm32"); - - ctx.platform = ctx.host_platform; } TEST_CASE("get_virtual_packages") From 254213f2df6ad6176894861a5816a8cfac122f4a Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Tue, 23 Jun 2026 17:14:34 +0200 Subject: [PATCH 3/4] Document `ScopedContextChange::preserve` Signed-off-by: Julien Jerphanion --- libmamba/tests/include/mambatests.hpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libmamba/tests/include/mambatests.hpp b/libmamba/tests/include/mambatests.hpp index 4bcd21868d..25cacf5671 100644 --- a/libmamba/tests/include/mambatests.hpp +++ b/libmamba/tests/include/mambatests.hpp @@ -98,10 +98,17 @@ namespace mambatests // destroyed. Repeated calls to the same setter only change the live value — the original // snapshot is always what gets restored. // + // Use set_* when the test assigns a known value. Use preserve() when the field will be + // changed later by code under test, by direct assignment (ctx.field = …), or by APIs such + // as Configuration::load() — preserve() only snapshots the current value for restoration. + // // Example: // auto& ctx = mambatests::context(); // mambatests::ScopedContextChange context_change{ ctx }; // context_change.set_channels({ "conda-forge" }).set_offline(false); + // + // context_change.preserve(&mamba::Context::use_sharded_repodata); + // ctx.use_sharded_repodata = false; // restored to the pre-preserve value at scope end class ScopedContextChange { public: @@ -176,6 +183,8 @@ namespace mambatests return *this; } + // Snapshot member for restoration without assigning a new value. The member pointer + // syntax (e.g. &mamba::Context::platform) selects which Context field to guard. template ScopedContextChange& preserve(T mamba::Context::* member) { From 737a84fde2adab7e24a5845fe0731c6a6f364f81 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Wed, 24 Jun 2026 11:33:07 +0200 Subject: [PATCH 4/4] Capture fields by reference and their initial value in lambda Signed-off-by: Julien Jerphanion Co-authored-by: Johan Mabille --- libmamba/tests/include/mambatests.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libmamba/tests/include/mambatests.hpp b/libmamba/tests/include/mambatests.hpp index 25cacf5671..4293ca2da0 100644 --- a/libmamba/tests/include/mambatests.hpp +++ b/libmamba/tests/include/mambatests.hpp @@ -205,8 +205,8 @@ namespace mambatests auto& field = m_ctx.*member; if (m_saved_fields.insert(static_cast(&field)).second) { - const T initial = field; - m_restorers.push_back([&field, initial] { field = initial; }); + m_restorers.push_back([&field, initial = field]() mutable + { field = std::move(initial); }); } mutator(field); }