diff options
| author | Simon Johnsson <simon.johnsson@canonical.com> | 2025-09-26 18:18:42 +0200 |
|---|---|---|
| committer | Julian Andres Klode <jak@debian.org> | 2025-09-26 16:18:42 +0000 |
| commit | 66962bee233b617e4a9ce436f9b26875511b35e6 (patch) | |
| tree | eb72d44c499404fd46c04119e7e5fb214f7833f2 | |
| parent | 3c9399e643a07074d47c9bceca88e8d43ff55d36 (diff) | |
History Command and Parsing
| -rw-r--r-- | apt-pkg/history.cc | 190 | ||||
| -rw-r--r-- | apt-pkg/history.h | 80 | ||||
| -rw-r--r-- | apt-private/private-history.cc | 239 | ||||
| -rw-r--r-- | apt-private/private-history.h | 11 | ||||
| -rw-r--r-- | cmdline/apt.cc | 20 | ||||
| -rwxr-xr-x | test/integration/test-history | 30 | ||||
| -rw-r--r-- | test/libapt/history_parse_test.cc | 113 |
7 files changed, 675 insertions, 8 deletions
diff --git a/apt-pkg/history.cc b/apt-pkg/history.cc new file mode 100644 index 000000000..4e838a016 --- /dev/null +++ b/apt-pkg/history.cc @@ -0,0 +1,190 @@ +// -*- mode: cpp; mode: fold -*- +// Description /*{{{*/ +/* ###################################################################### + Set of functions for parsing the history log + ##################################################################### */ +/*}}}*/ +// Include Files /*{{{*/ + +#include <config.h> + +#include <apt-pkg/configuration.h> +#include <apt-pkg/error.h> +#include <apt-pkg/fileutl.h> +#include <apt-pkg/history.h> +#include <apt-pkg/strutl.h> +#include <apt-pkg/tagfile.h> + +#include <algorithm> +#include <cctype> + +#include <glob.h> +#include <apti18n.h> // for coloring + +namespace APT::History +{ + +static Change ParsePackageEvent(const std::string &event) +{ + Change change{}; + // Remove all spaces + std::string trimEvent = event; + trimEvent.erase(std::remove_if(trimEvent.begin(), trimEvent.end(), + [](unsigned char c) + { return std::isspace(c); }), + trimEvent.end()); + auto openParen = trimEvent.find('('); + if (openParen == std::string::npos) + return change; + + change.package = trimEvent.substr(0, openParen); + + auto closeParen = trimEvent.find(')', openParen); + if (closeParen == std::string::npos) + return change; + + std::string versionStr = trimEvent.substr(openParen + 1, closeParen - openParen - 1); + auto values = VectorizeString(versionStr, ','); + for (size_t i = 0; i < values.size(); ++i) + { + if (i == 0 && std::isdigit(values[0][0])) + change.currentVersion = std::move(values[i]); + else if (i == 1 && std::isdigit(values[1][0])) + change.candidateVersion = std::move(values[i]); + else if (values[i] == "automatic") + change.automatic = true; + else + _error->Warning(_("Unknown flag: %s"), values[i].c_str()); + } + return change; +} + +static std::vector<std::string> SplitPackagesInContent(const std::string &content) +{ + std::vector<std::string> result; + std::size_t start = 0; + + if (content.length() == 0) + return result; + + // Split at "), " but keep the parenthesis. + while (true) + { + std::size_t pos = content.find("), ", start); + if (pos == std::string::npos) + { + result.push_back(content.substr(start)); + break; + } + result.push_back(content.substr(start, pos - start + 1)); + // Increment by 3 since 3 == len("), ") + start = pos + 3; + } + return result; +} + +std::string KindToString(const Kind &kind) +{ + switch (kind) + { + case Kind::Install: + return "Install"; + case Kind::Reinstall: + return "Reinstall"; + case Kind::Upgrade: + return "Upgrade"; + case Kind::Downgrade: + return "Downgrade"; + case Kind::Remove: + return "Remove"; + case Kind::Purge: + return "Purge"; + default: + return "Undefined"; + } +} + +Entry ParseSection( + const pkgTagSection §ion) +{ + Entry entry{}; + entry.startDate = section.Find("Start-Date"); + entry.endDate = section.Find("End-Date"); + entry.cmdLine = section.Find("Commandline"); + entry.requestingUser = section.Find("Requested-By"); + entry.comment = section.Find("Comment"); + entry.error = section.Find("Error"); + + std::string content = ""; + const Kind kinds[] = + { + Kind::Install, + Kind::Reinstall, + Kind::Downgrade, + Kind::Upgrade, + Kind::Remove, + Kind::Purge, + }; + for (const auto &kind : kinds) + { + content = section.Find(KindToString(kind)); + if (content.empty()) + continue; + + std::vector<std::string> package_events = SplitPackagesInContent(content); + std::vector<Change> changes = {}; + for (auto event : package_events) + { + Change change = ParsePackageEvent(event); + change.kind = kind; + changes.push_back(change); + } + // Changed packages should be in order + std::sort(changes.begin(), changes.end(), [](const Change &a, const Change &b) + { return a.package < b.package; }); + entry.changeMap[kind] = changes; + } + + return entry; +} + +bool ParseFile(FileFd &fd, HistoryBuffer &buf) +{ + pkgTagFile file(&fd, FileFd::ReadOnly); + pkgTagSection tmpSection; + while (file.Step(tmpSection)) + buf.push_back(ParseSection(tmpSection)); + return true; +} + +bool ParseLogDir(HistoryBuffer &buf) +{ + std::string files = _config->FindFile("Dir::Log::History") + "*"; + const char *pattern = files.data(); + glob_t result; + + int ret = glob(pattern, GLOB_TILDE, nullptr, &result); + if (ret != 0) + return _error->Error(_("Cannot find history files: %s"), files.c_str()); + + for (size_t i = 0; i < result.gl_pathc; ++i) + { + FileFd fd; + if (not fd.Open(result.gl_pathv[i], FileFd::ReadOnly, FileFd::Extension)) + return _error->Error(_("Could not open file: %s"), result.gl_pathv[i]); + if (not ParseFile(fd, buf)) + return _error->Error(_("Could not parse file: %s"), result.gl_pathv[i]); + if (not fd.Close()) + return _error->Error(_("Could not close file: %s"), result.gl_pathv[i]); + } + + // Sort entries by time + std::sort(buf.begin(), buf.end(), + [](const Entry &a, const Entry &b) + { + return a.startDate < b.startDate; + }); + + return true; +} +} // namespace APT::History diff --git a/apt-pkg/history.h b/apt-pkg/history.h new file mode 100644 index 000000000..08e7be388 --- /dev/null +++ b/apt-pkg/history.h @@ -0,0 +1,80 @@ +// -*- mode: cpp; mode: fold -*- +// Description /*{{{*/ +/* ###################################################################### + + History - Parse history logs to structured data. + + ##################################################################### */ +/*}}}*/ +#ifndef APTPKG_HISTORY_H +#define APTPKG_HISTORY_H + +#include <apt-pkg/fileutl.h> +#include <apt-pkg/macros.h> +#include <apt-pkg/tagfile.h> + +#include <map> +#include <string> +#include <vector> + +namespace APT::History +{ + +enum class Kind +{ + Install, + Reinstall, + Upgrade, + Downgrade, + Remove, + Purge, +}; + +// KindToString - Take a kind and return its +// string representation. +// +// NOTE: Non-localized English version +std::string KindToString(const Kind &kind); + +struct Change +{ + Kind kind; + std::string package; + std::string currentVersion; + std::string candidateVersion; + bool automatic = false; + + private: + void *d; // pointer for future extension; +}; + +struct Entry +{ + // Strings instead of string_view to avoid reference errors + std::string startDate; + std::string endDate; + std::string cmdLine; + std::string comment; + std::string error; + std::string requestingUser; + std::map<Kind, std::vector<Change>> changeMap; + + private: + void *d; +}; + +// History is defined as the collection of entries in the history log(s). +typedef std::vector<Entry> HistoryBuffer; + +// ParseSection - Take a tag section and parse it as a +// history log entry. +APT_PUBLIC Entry ParseSection(const pkgTagSection §ion); +// ParseFile - Take a file descriptor and parse it as a history +// log to the given buffer. +// NOTE: Caller is responsible for closing the file descriptor. +APT_PUBLIC bool ParseFile(FileFd &fd, HistoryBuffer &buf); +// ParseLogDir - Parse the apt history log directory to the buffer. +APT_PUBLIC bool ParseLogDir(HistoryBuffer &buf); +} // namespace APT::History + +#endif diff --git a/apt-private/private-history.cc b/apt-private/private-history.cc new file mode 100644 index 000000000..834ff3411 --- /dev/null +++ b/apt-private/private-history.cc @@ -0,0 +1,239 @@ +// Include files +#include <config.h> + +#include <apt-pkg/cmndline.h> +#include <apt-pkg/configuration.h> +#include <apt-pkg/error.h> +#include <apt-pkg/fileutl.h> +#include <apt-pkg/history.h> +#include <apt-pkg/tagfile.h> + +#include <apt-private/private-history.h> + +#include <iomanip> +#include <iostream> +#include <stdexcept> +#include <string> + +#include <glob.h> +#include <apti18n.h> // for coloring + /*}}}*/ + +using namespace APT::History; + +// ShortenCommand - Take a command and shorten it such that it adheres +// to the given maximum length. +static std::string ShortenCommand(const std::string &cmd, const std::size_t maxLen) +{ + std::string shortenedCmd = cmd; + if (shortenedCmd.starts_with("apt ")) + shortenedCmd = shortenedCmd.substr(4); + if (shortenedCmd.length() > maxLen - 3) + return shortenedCmd.substr(0, maxLen - 4) + "..."; + return shortenedCmd; +} + +static std::string LocalizeKindToString(const Kind &kind) +{ + switch (kind) + { + case Kind::Install: + return _("Install"); + case Kind::Reinstall: + return _("Reinstall"); + case Kind::Upgrade: + return _("Upgrade"); + case Kind::Downgrade: + return _("Downgrade"); + case Kind::Remove: + return _("Remove"); + case Kind::Purge: + return _("Purge"); + default: + return _("Undefined"); + } +} + +// GetKindString - Take a history entry and construct the string +// corresponding to the actions performed. Multpile actions are +// alphabetically grouped such as: +// Install, Remove, and Reinstall -> I,R,rI +// +// Shorthand legend: +// Install -> I +// Reinstall -> rI +// Upgrade -> U +// Downgrade -> D +// Remove -> R +// Purge -> P +// +static std::string GetKindString(const Entry &entry) +{ + // We want full output if there is only one action + if (entry.changeMap.size() == 1) + return LocalizeKindToString(entry.changeMap.begin()->first).data(); + + std::string kindGroup = ""; + // add localization later + for (const auto &[action, _] : entry.changeMap) + { + switch (action) + { + case Kind::Install: + kindGroup += "I"; + break; + case Kind::Reinstall: + kindGroup += "rI"; + break; + case Kind::Upgrade: + kindGroup += "U"; + break; + case Kind::Downgrade: + kindGroup += "D"; + break; + case Kind::Remove: + kindGroup += "R"; + break; + case Kind::Purge: + kindGroup += "P"; + break; + default: + kindGroup += "INVALID"; + } + kindGroup += ","; + } + // remove trailing "," + kindGroup.pop_back(); + return kindGroup; +} + +// PrintHistory - Take a history vector and print it. +static void PrintHistoryVector(const HistoryBuffer buf, int columnWidth) +{ + // Calculate the width of the ID column, esentially the number of digits + int id = 0; + size_t sizeFrac = buf.size(); + int idWidth = 2; + while (sizeFrac) + { + sizeFrac /= 10; + idWidth++; + } + + // Print headers + auto writeEntry = [](std::string entry, size_t width) + { + std::cout << std::left << std::setw(width) << entry; + }; + writeEntry(_("ID"), idWidth); + writeEntry(_("Command line"), columnWidth); + // NOTE: if date format is different, + // this width needs to change + writeEntry(_("Date and Time"), 23); // width for datestring + writeEntry(_("Action"), 10); // 9 chars longest action type + writeEntry(_("Changes"), columnWidth); + std::cout << "\n\n"; + + // Each entry corresponds to a row + for (auto entry : buf) + { + writeEntry(std::to_string(id), idWidth); + writeEntry(ShortenCommand(entry.cmdLine, columnWidth), columnWidth); + writeEntry(entry.startDate, 23); + // If there are multiple actions, we want to group them + writeEntry(GetKindString(entry), 10); + + // Count the number of packages changed + size_t numChanges = 0; + for (const auto &[_, changes] : entry.changeMap) + numChanges += changes.size(); + writeEntry(std::to_string(numChanges), columnWidth); + std::cout << "\n"; + id++; + } +} + +// PrintChange - Take a change and print the event for that package. +// Example: +// "package (0.1.0, 0.2.0)" and "Upgrade" -> "Upgrade package (0.1.0 -> 0.2.0)" +// "package (0.1.0)" and "Install" -> "Install package (0.1.0)" +static void PrintChange(const Change &change) +{ + std::cout << " " << LocalizeKindToString(change.kind) << " " << change.package; + std::cout << " (" << change.currentVersion; + if (not change.candidateVersion.empty()) + std::cout << " -> " << change.candidateVersion; + if (change.automatic) + std::cout << ", " << _("automatic"); + + std::cout << ")"; +} + +// PrintDetailedEntry - Take a history buffer and print the detailed +// transaction details for the given transaction id. +static void PrintDetailedEntry(const HistoryBuffer &buf, const size_t id) +{ + Entry entry = buf[id]; + std::cout << _("Transaction ID") << ": " << id << "\n"; + std::cout << _("Start time") << ": " << entry.startDate << "\n"; + std::cout << _("End time") << ": " << entry.endDate << "\n"; + std::cout << _("Requested by") << ": " << entry.requestingUser << "\n"; + std::cout << _("Command line") << ": " << entry.cmdLine << "\n"; + if (not entry.error.empty()) + { + std::cout << _config->Find("APT::Color::Red") << _("Error") << ": "; + std::cout << entry.error << _config->Find("APT::Color::Neutral") << "\n"; + } + if (not entry.comment.empty()) + std::cout << _("Comment") << ": " << entry.comment << "\n"; + + // For each performed action, print what it did to each package + std::cout << _("Packages changed") << ":" << "\n"; + for (const auto &[_, changes] : entry.changeMap) + { + for (const auto &change : changes) + { + PrintChange(change); + std::cout << "\n"; + } + std::cout << "\n"; + } +} + +bool DoHistoryList(CommandLine &Cmd) +{ + HistoryBuffer buf = {}; + + if (not ParseLogDir(buf)) + return _error->Error(_("Could not read: %s"), + _config->FindFile("Dir::Log::History").data()); + PrintHistoryVector(buf, 25); + + return true; +} + +bool DoHistoryInfo(CommandLine &Cmd) +{ + HistoryBuffer buf = {}; + if (not ParseLogDir(buf)) + return _error->Error(_("Could not read: %s"), + _config->FindFile("Dir::Log::History").data()); + + size_t id = 0; + for (size_t i = 1; i < Cmd.FileSize(); i++) + { + try + { + id = std::stoi(*(Cmd.FileList + i)); + } + catch (const std::invalid_argument &e) + { + return _error->Error(_("'%s' not a valid ID."), *(Cmd.FileList + i)); + } + if (buf.size() <= id) + return _error->Error(_("Transaction ID '%ld' out of bounds."), id); + PrintDetailedEntry(buf, id); + } + + return true; +} diff --git a/apt-private/private-history.h b/apt-private/private-history.h new file mode 100644 index 000000000..dfe0340f1 --- /dev/null +++ b/apt-private/private-history.h @@ -0,0 +1,11 @@ +#ifndef APT_PRIVATE_HISTORY_H +#define APT_PRIVATE_HISTORY_H + +#include <apt-pkg/cmndline.h> +#include <apt-pkg/history.h> +#include <apt-pkg/macros.h> +#include <apt-pkg/tagfile.h> + +APT_PUBLIC bool DoHistoryList(CommandLine &Cmd); +APT_PUBLIC bool DoHistoryInfo(CommandLine &Cmd); +#endif diff --git a/cmdline/apt.cc b/cmdline/apt.cc index a876085ce..545b6d0b4 100644 --- a/cmdline/apt.cc +++ b/cmdline/apt.cc @@ -22,6 +22,7 @@ #include <apt-private/private-cmndline.h> #include <apt-private/private-depends.h> #include <apt-private/private-download.h> +#include <apt-private/private-history.h> #include <apt-private/private-install.h> #include <apt-private/private-list.h> #include <apt-private/private-main.h> @@ -83,7 +84,7 @@ static std::vector<aptDispatchWithHelp> GetCommands() /*{{{*/ {"remove", &DoInstall, _("remove packages")}, {"autoremove", &DoInstall, _("automatically remove all unused packages")}, {"auto-remove", &DoInstall, nullptr}, - {"autopurge",&DoInstall, nullptr}, + {"autopurge", &DoInstall, nullptr}, {"purge", &DoInstall, nullptr}, // system wide stuff @@ -91,6 +92,10 @@ static std::vector<aptDispatchWithHelp> GetCommands() /*{{{*/ {"upgrade", &DoUpgrade, _("upgrade the system by installing/upgrading packages")}, {"full-upgrade", &DoDistUpgrade, _("upgrade the system by removing/installing/upgrading packages")}, + // history stuff + {"history-list", &DoHistoryList, _("show list of history")}, + {"history-info", &DoHistoryInfo, _("show info on specific transactions")}, + // misc {"edit-sources", &EditSources, _("edit the source information file")}, {"modernize-sources", &ModernizeSources, _("modernize .list files to .sources files")}, @@ -101,11 +106,11 @@ static std::vector<aptDispatchWithHelp> GetCommands() /*{{{*/ // for compat with muscle memory {"dist-upgrade", &DoDistUpgrade, nullptr}, - {"showsrc",&ShowSrcPackage, nullptr}, - {"depends",&Depends, nullptr}, - {"rdepends",&RDepends, nullptr}, - {"policy",&Policy, nullptr}, - {"build-dep", &DoBuildDep,nullptr}, + {"showsrc", &ShowSrcPackage, nullptr}, + {"depends", &Depends, nullptr}, + {"rdepends", &RDepends, nullptr}, + {"policy", &Policy, nullptr}, + {"build-dep", &DoBuildDep, nullptr}, {"clean", &DoClean, nullptr}, {"distclean", &DoDistClean, nullptr}, {"dist-clean", &DoDistClean, nullptr}, @@ -116,8 +121,7 @@ static std::vector<aptDispatchWithHelp> GetCommands() /*{{{*/ {"changelog", &DoChangelog, nullptr}, {"info", &ShowPackage, nullptr}, - {nullptr, nullptr, nullptr} - }; + {nullptr, nullptr, nullptr}}; } /*}}}*/ int main(int argc, const char *argv[]) /*{{{*/ diff --git a/test/integration/test-history b/test/integration/test-history index 47829e162..34489fce6 100755 --- a/test/integration/test-history +++ b/test/integration/test-history @@ -38,3 +38,33 @@ Commandline: dummy Comment: A test comment Install: pkg2:amd64 (1) End-Date: dummy" cathistory + +normalizedinfo() { + apt history-info 0 | sed -E \ + -e 's/^( *-?Start time *: ).*/\1dummy/' \ + -e 's/^( *-?End time *: ).*/\1dummy/' \ + -e 's|^( *-?Command line *: ).*|\1dummy|' +} + +testsuccessequal "Transaction ID: 0 +Start time: dummy +End time: dummy +Requested by: +Command line: dummy +Packages changed: + Install pkg1:amd64 (1) +" normalizedinfo + +# replace aribtrary command line and date with "dummy". +normalizedlist() { + apt history-list | sed -E 's/^([0-9]+)[[:space:]]+'\ +'[^[:space:]]+[[:space:]]+'\ +'[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]]+'\ +'[0-9]{2}:[0-9]{2}:[0-9]{2}/'\ +'\1 dummy dummy /' +} + +testsuccessequal "ID Command line Date and Time Action Changes + +0 dummy dummy Install 1 +1 dummy dummy Install 1 " normalizedlist diff --git a/test/libapt/history_parse_test.cc b/test/libapt/history_parse_test.cc new file mode 100644 index 000000000..526e1573f --- /dev/null +++ b/test/libapt/history_parse_test.cc @@ -0,0 +1,113 @@ +#include <config.h> + +#include <apt-pkg/configuration.h> +#include <apt-pkg/fileutl.h> +#include <apt-pkg/history.h> +#include <apt-pkg/indexcopy.h> +#include <apt-pkg/tagfile.h> + +#include "common.h" + +#include "file-helpers.h" + +using namespace APT::History; + +TEST(HistoryParseTest, SectionToEntry) +{ + FileFd fd; + const char *entry_section = + "Start-Date: 2025-09-01 15:22:56\n" + "Commandline: apt install rust-coreutils\n" + "Requested-By: user (1000)\n" + "Error: An error occurred\n" + "Comment: This is a comment\n" + "Install: rust-coreutils:amd64 (0.1.0+git20250813.4af2a84-0ubuntu2)\n" + "End-Date: 2025-09-01 15:22:57"; + + openTemporaryFile("packagesection", fd, entry_section); + + pkgTagFile tfile(&fd); + pkgTagSection section; + ASSERT_TRUE(tfile.Step(section)); + + Entry entry = ParseSection(section); + EXPECT_EQ("2025-09-01 15:22:56", entry.startDate); + EXPECT_EQ("2025-09-01 15:22:57", entry.endDate); + EXPECT_EQ("apt install rust-coreutils", entry.cmdLine); + EXPECT_EQ("user (1000)", entry.requestingUser); + EXPECT_EQ("An error occurred", entry.error); + EXPECT_EQ("This is a comment", entry.comment); + EXPECT_EQ("rust-coreutils:amd64", entry.changeMap[Kind::Install][0].package); + EXPECT_EQ("0.1.0+git20250813.4af2a84-0ubuntu2", entry.changeMap[Kind::Install][0].currentVersion); +} + +TEST(HistoryParseTest, EmptyOptionalFields) +{ + FileFd fd; + const char *entry_section = + "Start-Date: 2025-09-01 15:22:56\n" + "Commandline: apt install rust-coreutils\n" + "Requested-By: user (1000)\n" + "Install: rust-coreutils:amd64 (0.1.0+git20250813.4af2a84-0ubuntu2)\n" + "End-Date: 2025-09-01 15:22:57"; + openTemporaryFile("packagesection", fd, entry_section); + pkgTagFile tfile(&fd); + pkgTagSection section; + ASSERT_TRUE(tfile.Step(section)); + + Entry entry = ParseSection(section); + EXPECT_EQ("", entry.error); + EXPECT_EQ("", entry.comment); +} + +TEST(HistoryParseTest, MultipleActions) +{ + FileFd fd; + const char *entry_section = + "Start-Date: 2025-09-01 15:22:56\n" + "Commandline: apt install rust-coreutils\n" + "Requested-By: user (1000)\n" + "Error: An error occurred\n" + "Comment: This is a comment\n" + "Install: rust-coreutils:amd64 (0.1.0+git20250813.4af2a84-0ubuntu2)\n" + "Remove: rust-coreutils:amd64 (0.1.0+git20250813.4af2a84-0ubuntu2)\n" + "Downgrade: rust-coreutils:amd64 (0.1.0, 0.0.0)\n" + "Reinstall: rust-coreutils:amd64 (0.1.0+git20250813.4af2a84-0ubuntu2)\n" + "Upgrade: rust-coreutils:amd64 (0.1.0, 0.2.0)\n" + "Purge: rust-coreutils:amd64 (0.1.0+git20250813.4af2a84-0ubuntu2)\n" + "End-Date: 2025-09-01 15:22:57"; + + openTemporaryFile("packagesection", fd, entry_section); + + pkgTagFile tfile(&fd); + pkgTagSection section; + ASSERT_TRUE(tfile.Step(section)); + + Entry entry = ParseSection(section); + Change installChange = entry.changeMap[Kind::Install][0]; + Change removeChange = entry.changeMap[Kind::Remove][0]; + Change downgradeChange = entry.changeMap[Kind::Downgrade][0]; + Change reinstallChange = entry.changeMap[Kind::Reinstall][0]; + Change upgradeChange = entry.changeMap[Kind::Upgrade][0]; + Change purgeChange = entry.changeMap[Kind::Purge][0]; + + EXPECT_EQ("rust-coreutils:amd64", installChange.package); + EXPECT_EQ("0.1.0+git20250813.4af2a84-0ubuntu2", installChange.currentVersion); + + EXPECT_EQ("rust-coreutils:amd64", removeChange.package); + EXPECT_EQ("0.1.0+git20250813.4af2a84-0ubuntu2", removeChange.currentVersion); + + EXPECT_EQ("rust-coreutils:amd64", downgradeChange.package); + EXPECT_EQ("0.1.0", downgradeChange.currentVersion); + EXPECT_EQ("0.0.0", downgradeChange.candidateVersion); + + EXPECT_EQ("rust-coreutils:amd64", reinstallChange.package); + EXPECT_EQ("0.1.0+git20250813.4af2a84-0ubuntu2", reinstallChange.currentVersion); + + EXPECT_EQ("rust-coreutils:amd64", upgradeChange.package); + EXPECT_EQ("0.1.0", upgradeChange.currentVersion); + EXPECT_EQ("0.2.0", upgradeChange.candidateVersion); + + EXPECT_EQ("rust-coreutils:amd64", purgeChange.package); + EXPECT_EQ("0.1.0+git20250813.4af2a84-0ubuntu2", purgeChange.currentVersion); +} |
