Files
OrcaSlicer/tests/slic3rutils/test_plugin_install.cpp
T
Riccardo BRAMATI 07a12f5fe4 Read a local plugin's version from its file header (#16118)
* Read a local plugin's version from its file header

read_install_state() let the sidecar's installed_version override the version
parsed from the entry file's PEP 723 header. That is right for cloud plugins,
whose header may be stale, but a local plugin has no other source of truth, so
the UI kept showing the version recorded at first install after the file was
edited. For local installs, use the header version.

* Keep the cloud installed version of an unsubscribed plugin

A cloud plugin kept as local after unsubscribing is rewritten with installed_from=local and no cloud_uuid, which the local-header rule could not tell apart from a real local install. Its installed_version is still the cloud one while the header may lag, so the next scan showed the stale header version. Record the conversion in the install state and exclude those plugins from the header rule. Add tests for a local plugin following its header and for an unsubscribed cloud plugin keeping its cloud version.

* Add tests for the version of a local plugin

A local plugin's version comes from its entry file header. Cover the case of a plugin edited after install, and a plugin unsubscribed from the cloud, which is now a normal local package.
2026-10-11 03:09:32 +08:00

187 lines
7.2 KiB
C++

#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <catch2/catch_all.hpp>
#include <ios>
#include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "plugin_test_utils.hpp"
#include <boost/filesystem.hpp>
#include <fstream>
#include <string>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace {
fs::path write_py_file(const fs::path& dir, const std::string& filename, const std::string& contents)
{
fs::create_directories(dir);
const fs::path p = dir / filename;
std::ofstream out(p.string(), std::ios::binary);
out << contents;
return p;
}
} // namespace
TEST_CASE("install_plugin rejects a cloud UUID containing path traversal", "[PluginInstall]")
{
ScopedDataDir data_dir_guard("cor2");
// Package contents are irrelevant: the UUID is validated before metadata is read.
const fs::path py = write_py_file(data_dir_guard.dir / "src", "evil.py", "print('hi')\n");
PluginDescriptor descriptor;
// is_cloud_plugin() -> true; cloud_uuid() -> the traversal string.
descriptor.cloud = CloudPluginState{"../../escape", true, false, false, false};
std::string error;
const bool installed = plugin_loader::install_plugin(py, /*cloud_user_id=*/"test-user", descriptor, error);
REQUIRE_FALSE(installed);
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("valid identifier"));
}
TEST_CASE("install_plugin rejects a side-loaded .py with no PEP 723 metadata", "[PluginInstall]")
{
ScopedDataDir data_dir_guard("cor3-bad");
// No `# /// script` block -> name stays empty and type stays Unknown.
const fs::path py = write_py_file(data_dir_guard.dir / "src", "nameless.py", "print('no metadata here')\n");
PluginDescriptor descriptor;
std::string error;
const bool installed = plugin_loader::install_plugin(py, /*cloud_user_id=*/"", descriptor, error);
REQUIRE_FALSE(installed);
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("PEP 723"));
}
TEST_CASE("install_plugin accepts a side-loaded .py with complete PEP 723 metadata", "[PluginInstall]")
{
ScopedDataDir data_dir_guard("cor3-good");
const std::string contents =
"# /// script\n"
"# requires-python = \">=3.12\"\n"
"#\n"
"# [tool.orcaslicer.plugin]\n"
"# name = \"Test Plugin\"\n"
"# type = \"script\"\n"
"# ///\n"
"print('ok')\n";
const fs::path py = write_py_file(data_dir_guard.dir / "src", "good.py", contents);
PluginDescriptor descriptor;
std::string error;
const bool installed = plugin_loader::install_plugin(py, /*cloud_user_id=*/"", descriptor, error);
// Positive control: a complete side-loaded .py must still install (guards against over-rejection).
REQUIRE(installed);
CHECK(error.empty());
}
TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's installed version", "[PluginInstall]")
{
ScopedDataDir data_dir_guard("installed-version");
const fs::path plugin_dir = data_dir_guard.dir / "plugin";
fs::create_directories(plugin_dir);
// A cloud plugin whose local manifest/PEP723 header lags the version actually fetched from
// the cloud: the user bumped the version on the cloud without touching the local header.
PluginDescriptor descriptor;
descriptor.name = "Versioned Plugin";
descriptor.version = "1.0.0"; // stale header version
descriptor.installed_version = "1.2.0"; // version fetched from the cloud at install time
descriptor.cloud = CloudPluginState{"uuid-1", true, false, false, false};
REQUIRE(write_install_state(plugin_dir, descriptor));
// The writer must persist the installed_version (1.2.0), not the header version (1.0.0),
// so a subsequent re-write from a freshly-scanned descriptor cannot clobber it.
PluginInstallState state;
REQUIRE(read_install_state(plugin_dir, state));
CHECK(state.installed_version == "1.2.0");
state.permissions.fs_read = {"/path/to/read"};
state.permissions.fs_readwrite = {"/path/to/readwrite"};
state.permissions.network_http = {"https://api.example.com"};
state.permissions.network_socket = {"192.168.45.6:443"};
state.permissions.process = {"/usr/bin/curl"};
state.permissions.threading = {"thread"};
REQUIRE(write_install_state(plugin_dir, state));
// Permission data is persisted in the same sidecar as the installation metadata.
PluginInstallState persisted;
REQUIRE(read_install_state(plugin_dir, persisted));
CHECK(persisted.permissions.fs_read == state.permissions.fs_read);
CHECK(persisted.permissions.fs_readwrite == state.permissions.fs_readwrite);
CHECK(persisted.permissions.network_http == state.permissions.network_http);
CHECK(persisted.permissions.network_socket == state.permissions.network_socket);
CHECK(persisted.permissions.process == state.permissions.process);
CHECK(persisted.permissions.threading == state.permissions.threading);
// Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still
// 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare
// the cloud's latest version against the installed version instead of the stale header, so an
// already-updated plugin no longer looks perpetually out of date.
PluginDescriptor scanned;
scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header
read_install_state(plugin_dir, scanned);
CHECK(scanned.installed_version == "1.2.0");
}
TEST_CASE("a local plugin's installed version follows its entry file header", "[PluginInstall]")
{
ScopedDataDir data_dir_guard("local-installed-version");
const fs::path plugin_dir = data_dir_guard.dir / "plugin";
fs::create_directories(plugin_dir);
// Sidecar written when the local plugin was first installed at 1.0.0.
PluginInstallState state;
state.installed_from = "local";
state.installed_version = "1.0.0";
REQUIRE(write_install_state(plugin_dir, state));
// The user then edited the plugin file: the freshly scanned header says 1.1.0.
PluginDescriptor scanned;
scanned.version = "1.1.0";
REQUIRE(read_install_state(plugin_dir, scanned));
CHECK(scanned.installed_version == "1.1.0");
}
TEST_CASE("an unsubscribed cloud plugin follows its header version", "[PluginInstall]")
{
ScopedDataDir data_dir_guard("unsubscribed-installed-version");
const fs::path plugin_dir = data_dir_guard.dir / "plugin";
fs::create_directories(plugin_dir);
// What keep_installed_plugin_as_local leaves behind: a normal local package. The sidecar still
// holds the version fetched from the cloud (1.2.0), but the header (1.0.0) is the source of truth.
PluginInstallState state;
state.installed_from = "local";
state.installed_version = "1.2.0";
REQUIRE(write_install_state(plugin_dir, state));
PluginDescriptor scanned;
scanned.version = "1.0.0";
REQUIRE(read_install_state(plugin_dir, scanned));
CHECK(scanned.installed_version == "1.0.0");
}