Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ build-macos/
build-ohos/
build-windows/
cmake-build-debug/
_codeql_build_dir/
_codeql_detected_source_root

# Generated test binaries and object files
*.o
Expand Down
6 changes: 6 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
add_executable(url_opener_test url_opener_test.cpp)
target_link_libraries(url_opener_test PRIVATE nativeapi)
add_test(NAME url_opener_test COMMAND url_opener_test)

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(autostart_test autostart_test.cpp)
target_link_libraries(autostart_test PRIVATE nativeapi)
add_test(NAME autostart_test COMMAND autostart_test)
endif()
350 changes: 350 additions & 0 deletions tests/autostart_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
#include <sys/stat.h>
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<sys/stat.h> is included but not used anywhere in this test file. Removing unused headers keeps build times and dependencies minimal.

Suggested change
#include <sys/stat.h>

Copilot uses AI. Check for mistakes.
#include <unistd.h>

#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include "../src/autostart.h"

namespace {

// Return a temporary XDG_CONFIG_HOME directory unique to this test run
static std::string MakeTempConfigDir() {
const char* tmpbase = std::getenv("TMPDIR");
if (!tmpbase || !*tmpbase)
tmpbase = "/tmp";
std::string tmpl = std::string(tmpbase) + "/autostart_test_XXXXXX";
const char* dir = mkdtemp(tmpl.data());
if (!dir) {
std::cerr << "mkdtemp failed" << std::endl;
return std::string();
}
return std::string(dir);
}

// Remove a directory tree (non-recursively safe: only removes known files).
// Failures are intentionally ignored since this is test cleanup only.
static void RemoveDesktopFile(const std::string& config_dir, const std::string& id) {
std::string path = config_dir + "/autostart/" + id + ".desktop";
unlink(path.c_str());
rmdir((config_dir + "/autostart").c_str());
rmdir(config_dir.c_str());
}

// Read file contents into a string
static std::string ReadFile(const std::string& path) {
std::ifstream ifs(path);
if (!ifs.is_open())
return std::string();
std::ostringstream oss;
oss << ifs.rdbuf();
return oss.str();
}

// Scoped XDG_CONFIG_HOME override to isolate tests
struct ScopedConfigHome {
std::string dir;
std::string prev;

explicit ScopedConfigHome(const std::string& d) : dir(d) {
const char* p = std::getenv("XDG_CONFIG_HOME");
prev = p ? std::string(p) : std::string();
setenv("XDG_CONFIG_HOME", dir.c_str(), 1);
}

~ScopedConfigHome() {
if (prev.empty()) {
unsetenv("XDG_CONFIG_HOME");
} else {
setenv("XDG_CONFIG_HOME", prev.c_str(), 1);
}
}
};

int RunTests() {
using namespace nativeapi;

// IsSupported returns true on Linux
{
if (!AutoStart::IsSupported()) {
std::cerr << "AutoStart::IsSupported() should return true on Linux." << std::endl;
return 1;
}
}

// Constructors and getters
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

AutoStart as("com.example.testapp", "TestApp");
if (as.GetId() != "com.example.testapp") {
std::cerr << "GetId() should return 'com.example.testapp', got '" << as.GetId() << "'"
<< std::endl;
rmdir(tmpDir.c_str());
return 1;
}
if (as.GetDisplayName() != "TestApp") {
std::cerr << "GetDisplayName() should return 'TestApp', got '" << as.GetDisplayName() << "'"
<< std::endl;
rmdir(tmpDir.c_str());
return 1;
}
rmdir(tmpDir.c_str());
}

// SetDisplayName
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

AutoStart as("com.example.testapp", "OriginalName");
if (!as.SetDisplayName("NewName")) {
std::cerr << "SetDisplayName() should return true." << std::endl;
rmdir(tmpDir.c_str());
return 1;
}
if (as.GetDisplayName() != "NewName") {
std::cerr << "GetDisplayName() after SetDisplayName() should return 'NewName', got '"
<< as.GetDisplayName() << "'" << std::endl;
rmdir(tmpDir.c_str());
return 1;
}
rmdir(tmpDir.c_str());
}

// SetProgram / GetExecutablePath / GetArguments
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

AutoStart as("com.example.testapp", "TestApp");
std::vector<std::string> args = {"--minimized", "--tray"};
if (!as.SetProgram("/usr/bin/testapp", args)) {
std::cerr << "SetProgram() should return true." << std::endl;
rmdir(tmpDir.c_str());
return 1;
}
if (as.GetExecutablePath() != "/usr/bin/testapp") {
std::cerr << "GetExecutablePath() should return '/usr/bin/testapp', got '"
<< as.GetExecutablePath() << "'" << std::endl;
rmdir(tmpDir.c_str());
return 1;
}
auto gotArgs = as.GetArguments();
if (gotArgs != args) {
std::cerr << "GetArguments() returned unexpected values." << std::endl;
rmdir(tmpDir.c_str());
return 1;
}
rmdir(tmpDir.c_str());
}

// Enable creates .desktop file and IsEnabled returns true
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

const std::string testId = "com.example.autostarttest";
AutoStart as(testId, "AutoStartTest");
as.SetProgram("/usr/bin/testapp", {});

if (!as.Enable()) {
std::cerr << "Enable() should return true." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

if (!as.IsEnabled()) {
std::cerr << "IsEnabled() should return true after Enable()." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

RemoveDesktopFile(tmpDir, testId);
}

// Disable removes .desktop file and IsEnabled returns false
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

const std::string testId = "com.example.autostarttest";
AutoStart as(testId, "AutoStartTest");
as.SetProgram("/usr/bin/testapp", {});

if (!as.Enable()) {
std::cerr << "Enable() should return true (precondition for Disable test)." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

if (!as.Disable()) {
std::cerr << "Disable() should return true." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

if (as.IsEnabled()) {
std::cerr << "IsEnabled() should return false after Disable()." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

RemoveDesktopFile(tmpDir, testId);
}

// Disable is idempotent (no error when file does not exist)
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

AutoStart as("com.example.autostarttest", "AutoStartTest");
if (!as.Disable()) {
std::cerr << "Disable() on non-existent entry should return true (idempotent)." << std::endl;
rmdir(tmpDir.c_str());
return 1;
}
rmdir(tmpDir.c_str());
}

// IsEnabled returns false before Enable() is called
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

AutoStart as("com.example.autostarttest", "AutoStartTest");
if (as.IsEnabled()) {
std::cerr << "IsEnabled() should return false before Enable() is called." << std::endl;
rmdir(tmpDir.c_str());
return 1;
}
rmdir(tmpDir.c_str());
}

// Enable() writes valid .desktop content
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

const std::string testId = "com.example.contenttest";
AutoStart as(testId, "ContentTestApp");
as.SetProgram("/usr/local/bin/myapp", {"--flag", "value with space"});

if (!as.Enable()) {
std::cerr << "Enable() should return true." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

std::string desktopPath = tmpDir + "/autostart/" + testId + ".desktop";
std::string content = ReadFile(desktopPath);

if (content.find("[Desktop Entry]") == std::string::npos) {
std::cerr << "Desktop file missing [Desktop Entry] section." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
if (content.find("Type=Application") == std::string::npos) {
std::cerr << "Desktop file missing 'Type=Application'." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
if (content.find("Name=ContentTestApp") == std::string::npos) {
std::cerr << "Desktop file missing 'Name=ContentTestApp'." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
if (content.find("Exec=") == std::string::npos) {
std::cerr << "Desktop file missing 'Exec=' line." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
if (content.find("/usr/local/bin/myapp") == std::string::npos) {
std::cerr << "Desktop file Exec line missing executable path." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .desktop content test exercises an argument containing spaces ("value with space"), but it doesn't assert that the generated Exec= line actually preserves it as a single argument (i.e., is quoted/escaped correctly). Adding a check that the Exec line contains the properly quoted/escaped argument would better validate the intended XDG quoting behavior.

Suggested change
}
}
// Verify that the Exec line preserves the spaced argument as a single argument
std::size_t execPos = content.find("Exec=");
if (execPos != std::string::npos) {
std::size_t endOfLine = content.find('\n', execPos);
std::string execLine =
content.substr(execPos, endOfLine == std::string::npos ? std::string::npos
: endOfLine - execPos);
if (execLine.find("\"value with space\"") == std::string::npos &&
execLine.find("value\\ with\\ space") == std::string::npos) {
std::cerr << "Exec line does not preserve spaced argument correctly: "
<< execLine << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
}

Copilot uses AI. Check for mistakes.
if (content.find("Hidden=false") == std::string::npos) {
std::cerr << "Desktop file missing 'Hidden=false'." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
if (content.find("X-GNOME-Autostart-enabled=true") == std::string::npos) {
std::cerr << "Desktop file missing 'X-GNOME-Autostart-enabled=true'." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

RemoveDesktopFile(tmpDir, testId);
}

// Enable() then Enable() again updates the entry (re-enables)
{
const std::string tmpDir = MakeTempConfigDir();
if (tmpDir.empty())
return 1;
ScopedConfigHome scope(tmpDir);

const std::string testId = "com.example.autostarttest";
AutoStart as(testId, "AutoStartTest");
as.SetProgram("/usr/bin/testapp", {});

if (!as.Enable()) {
std::cerr << "Enable() first call should return true." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
// Update program and enable again
as.SetProgram("/usr/bin/testapp2", {});
if (!as.Enable()) {
std::cerr << "Enable() second call should return true." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}
if (!as.IsEnabled()) {
std::cerr << "IsEnabled() should return true after second Enable()." << std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

std::string desktopPath = tmpDir + "/autostart/" + testId + ".desktop";
std::string content = ReadFile(desktopPath);
if (content.find("/usr/bin/testapp2") == std::string::npos) {
std::cerr << "Desktop file should reference updated executable after second Enable()."
<< std::endl;
RemoveDesktopFile(tmpDir, testId);
return 1;
}

RemoveDesktopFile(tmpDir, testId);
}

return 0;
}

} // namespace

int main() {
return RunTests();
}