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
3 changes: 2 additions & 1 deletion Tests/Utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
set(TEST_SOURCES
TestSed.cpp
TestPatch.cpp
TestSed.cpp
TestTest.cpp
TestUniq.cpp
)

Expand Down
34 changes: 34 additions & 0 deletions Tests/Utilities/TestTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2026, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/StringView.h>
#include <LibCore/File.h>
#include <LibCore/Process.h>
#include <LibFileSystem/TempFile.h>
#include <LibTest/TestCase.h>

static void run_test(Vector<ByteString>&& arguments, int expected_exit_status)
{
auto test = TRY_OR_FAIL(Core::Process::spawn(
Core::ProcessSpawnOptions { .executable = "test"sv,
.search_for_executable_in_path = true,
.arguments = arguments }));
auto exited_with_code_0 = TRY_OR_FAIL(test.wait_for_termination());
EXPECT_EQ(expected_exit_status, exited_with_code_0 ? 0 : 1);
}

TEST_CASE(option_s)
{
run_test({ "-s", "file_that_do_not_exist" }, 1);

// empty file
auto temp_file = TRY_OR_FAIL(FileSystem::TempFile::create_temp_file());
run_test({ "-s", MUST(ByteString::from_utf8(temp_file->path())) }, 1);

auto file = TRY_OR_FAIL(Core::File::open(temp_file->path(), Core::File::OpenMode::Write));
TRY_OR_FAIL(file->write_until_depleted("file content\n"sv));
run_test({ "-s", MUST(ByteString::from_utf8(temp_file->path())) }, 0);
}
25 changes: 24 additions & 1 deletion Userland/Utilities/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,28 @@ class FileIsOwnedBy : public Condition {
Owner m_kind { EffectiveGID };
};

class FileExists : public Condition {
public:
FileExists(StringView path)
: m_path(path)
{
}

private:
virtual bool check() const override
{
// "True if pathname resolves to an existing directory entry for a file that
// has a size greater than zero. False if pathname cannot be resolved, or if
// pathname resolves to an existing directory entry for a file that does not
// have a size greater than zero."

auto result = Core::System::stat(m_path);
return !result.is_error() && result.value().st_size > 0;
}

ByteString m_path;
};

class StringCompare : public Condition {
public:
enum Mode {
Expand Down Expand Up @@ -478,8 +500,9 @@ static OwnPtr<Condition> parse_simple_expression(char* argv[])
return make<FileIsOwnedBy>(value, FileIsOwnedBy::EffectiveGID);
case 'O':
return make<FileIsOwnedBy>(value, FileIsOwnedBy::EffectiveUID);
case 'N':
case 's':
return make<FileExists>(value);
case 'N':
// 'optind' has been incremented to refer to the argument after the
// operator, while we want to print the operator itself.
fatal_error("Unsupported operator \033[1m%s", argv[optind - 1]);
Expand Down
Loading