AnyPS5/core/libs/tests/GuestGetopt.cpp

368 lines
16 KiB
C++

#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <string>
#include <vector>
struct GuestOption {
const char* name;
int hasArgument;
int* flag;
int value;
};
extern "C" {
int APS5_VABI getopt_nid_postfix(int, char* const*, const char*);
int APS5_VABI getopt_long_nid_postfix(int, char* const*, const char*, const GuestOption*, int*);
int APS5_VABI setenv_nid_postfix(const char*, const char*, int);
int APS5_VABI unsetenv_nid_postfix(const char*);
extern int opterr_nid_postfix;
extern int optind_nid_postfix;
extern int optopt_nid_postfix;
extern int optreset_nid_postfix;
extern char* optarg_nid_postfix;
extern FileStream* __stderrp_nid_postfix;
}
namespace {
void Expect(bool value, const std::string& what) {
if (value) return;
std::fprintf(stderr, "failed: %s\n", what.c_str());
std::abort();
}
void ExpectText(const std::string& actual, const std::string& expected, const std::string& what) {
Expect(actual == expected, what + ": `" + actual + "' != `" + expected + "'");
}
class ArgumentVector {
std::vector<std::string> storage;
std::vector<char*> pointers;
public:
explicit ArgumentVector(const std::string& line) {
std::istringstream words(line);
for (std::string word; words >> word;) storage.push_back(word);
for (auto& word : storage) pointers.push_back(word.data());
pointers.push_back(nullptr);
}
int Count() const { return static_cast<int>(storage.size()); }
char* const* Values() { return pointers.data(); }
std::string Joined() const {
std::string result;
for (std::size_t i = 0; i + 1 < pointers.size(); ++i) result += (i ? " " : "") + std::string(pointers[i]);
return result;
}
};
class StderrCapture {
FileStream* previous;
FileStream stream;
public:
StderrCapture() : previous(__stderrp_nid_postfix), stream(std::tmpfile()) { __stderrp_nid_postfix = &stream; }
~StderrCapture() {
__stderrp_nid_postfix = previous;
stream.Close();
}
std::string Text() {
std::FILE* handle = stream.GetHandle();
std::fflush(handle);
std::rewind(handle);
std::string text;
for (int character; (character = std::fgetc(handle)) != EOF;) text.push_back(static_cast<char>(character));
return text;
}
};
void Reset() {
optind_nid_postfix = optreset_nid_postfix = 1;
opterr_nid_postfix = 1;
}
std::string Finish(std::string result, int count) {
if (result.empty()) result = "|";
else result.back() = '|';
return result + std::to_string(count - optind_nid_postfix);
}
std::string RunGetopt(const char* options, ArgumentVector& arguments) {
Reset();
std::string result;
for (int character; (character = getopt_nid_postfix(arguments.Count(), arguments.Values(), options)) != -1;) {
Expect(character != ':', "getopt returned `:'");
const char* found = std::strchr(options, character);
if (!found) result += "!" + std::string(1, static_cast<char>(character)) + ",";
else if (found[1] != ':') result += std::string(1, static_cast<char>(character)) + ",";
else result += std::string(1, static_cast<char>(character)) + "=" + optarg_nid_postfix + ",";
}
return Finish(result, arguments.Count());
}
std::string RunGetoptLong(const char* options, const GuestOption* longOptions, ArgumentVector& arguments) {
Reset();
std::string result;
int index = -2;
for (int character; (character = getopt_long_nid_postfix(arguments.Count(), arguments.Values(), options, longOptions, &index)) != -1;) {
Expect(character != ':', "getopt_long returned `:'");
if (index == -2) {
const char* found = std::strchr(options, character);
if (!found) result += "!" + std::string(1, static_cast<char>(character)) + ",";
else if (found[1] != ':') result += std::string(1, static_cast<char>(character)) + ",";
else result += std::string(1, static_cast<char>(character)) + "=" + optarg_nid_postfix + ",";
} else {
const GuestOption& option = longOptions[index];
result += "-" + std::string(option.name);
if (option.hasArgument == 1) result += "=" + std::string(optarg_nid_postfix);
if (option.hasArgument == 2 && optarg_nid_postfix) result += "=" + std::string(optarg_nid_postfix);
result += ",";
}
index = -2;
}
return Finish(result, arguments.Count());
}
struct GetoptCase {
const char* options;
const char* arguments;
const char* result;
};
void FreeBsdGetoptCases() {
const GetoptCase cases[] = {
{"c:d", "foo -c 1 -d foo", "c=1,d|1"},
{"c:d", "foo -d foo bar", "d|2"},
{"c:d", "foo -c 2 foo bar", "c=2|2"},
{"c:d", "foo -e 1 foo bar", "!?|3"},
{"c:d", "foo -d -- -c 1", "d|2"},
{"c:d", "foo -c- 1", "c=-|1"},
{"c:d", "foo -d - 1", "d|2"},
};
for (const auto& test : cases) {
ArgumentVector arguments(test.arguments);
StderrCapture capture;
ExpectText(RunGetopt(test.options, arguments), test.result, std::string("getopt ") + test.arguments);
const std::string expectedError = std::strcmp(test.result, "!?|3") == 0 ? "eboot.bin: illegal option -- e\n" : "";
ExpectText(capture.Text(), expectedError, std::string("getopt stderr ") + test.arguments);
}
}
void FreeBsdGetoptLongCases() {
const GuestOption gnu[] = {
{"required", 1, nullptr, 'r'},
{"optional", 2, nullptr, 'o'},
{"none", 0, nullptr, 'n'},
{"color", 0, nullptr, 'C'},
{"colour", 0, nullptr, 'C'},
{nullptr, 0, nullptr, 0},
};
const GetoptCase cases[] = {
{"abc:", "foo --req foobar", "-required=foobar|0"},
{"abc:", "foo --opt=bazbug", "-optional=bazbug|0"},
{"abc:", "foo --col", "-color|0"},
{"abc:", "foo --colour", "-colour|0"},
{"abc:", "foo -a -b -cfoobar --required foobar --optional=bazbug --none random --col --color --colour",
"a,b,c=foobar,-required=foobar,-optional=bazbug,-none,-color,-color,-colour|1"},
};
for (const auto& test : cases) {
ArgumentVector arguments(test.arguments);
StderrCapture capture;
ExpectText(RunGetoptLong(test.options, gnu, arguments), test.result, std::string("getopt_long ") + test.arguments);
ExpectText(capture.Text(), "", std::string("getopt_long stderr ") + test.arguments);
}
int flags[3] {};
const GuestOption lists[] = {
{"list-foobar", 0, &flags[0], 'f'},
{"list-goobar", 0, &flags[1], 'g'},
{"list", 0, &flags[2], 'l'},
{nullptr, 0, nullptr, 0},
};
ArgumentVector arguments("foo --list");
ExpectText(RunGetoptLong("fgl", lists, arguments), "-list|0", "exact match among prefixes");
Expect(flags[0] == 0 && flags[1] == 0 && flags[2] == 'l', "flag set by an exact match");
}
void ShortOptions() {
ArgumentVector grouped("foo -dcvalue -dd rest");
ExpectText(RunGetopt("c:d", grouped), "d,c=value,d,d|1", "grouped options");
ArgumentVector optional("foo -afoo -a bar");
Reset();
Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == 'a' && std::strcmp(optarg_nid_postfix, "foo") == 0, "optional attached");
Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == 'a' && optarg_nid_postfix == nullptr && optind_nid_postfix == 3, "optional absent");
Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == -1 && optind_nid_postfix == 3, "optional stops at operand");
ArgumentVector dash("foo - -x");
Reset();
Expect(getopt_nid_postfix(dash.Count(), dash.Values(), "-x") == '-' && optind_nid_postfix == 2, "lone dash as option");
Expect(getopt_nid_postfix(dash.Count(), dash.Values(), "-x") == 'x' && optind_nid_postfix == 3, "after lone dash");
ArgumentVector unknown("foo -x");
{
StderrCapture capture;
Reset();
opterr_nid_postfix = 0;
Expect(getopt_nid_postfix(unknown.Count(), unknown.Values(), "ab") == '?', "unknown option returns ?");
Expect(optopt_nid_postfix == 'x' && optind_nid_postfix == 2, "optopt of unknown option");
ExpectText(capture.Text(), "", "opterr 0 is silent");
}
{
StderrCapture capture;
Reset();
Expect(getopt_nid_postfix(unknown.Count(), unknown.Values(), ":ab") == '?' && optopt_nid_postfix == 'x', "leading colon unknown");
ExpectText(capture.Text(), "", "leading colon is silent");
}
ArgumentVector missing("foo -c");
{
StderrCapture capture;
Reset();
Expect(getopt_nid_postfix(missing.Count(), missing.Values(), "c:") == '?' && optopt_nid_postfix == 'c' && optind_nid_postfix == 2, "missing argument");
ExpectText(capture.Text(), "eboot.bin: option requires an argument -- c\n", "missing argument message");
}
{
StderrCapture capture;
Reset();
Expect(getopt_nid_postfix(missing.Count(), missing.Values(), ":c:") == ':' && optopt_nid_postfix == 'c', "missing argument with leading colon");
ExpectText(capture.Text(), "", "leading colon missing argument is silent");
}
ArgumentVector first("foo -ab");
ArgumentVector second("bar -c");
Reset();
Expect(getopt_nid_postfix(first.Count(), first.Values(), "abc") == 'a', "first parse");
optind_nid_postfix = optreset_nid_postfix = 1;
Expect(getopt_nid_postfix(second.Count(), second.Values(), "abc") == 'c', "optreset restarts getopt");
Expect(getopt_nid_postfix(second.Count(), second.Values(), "abc") == -1 && optind_nid_postfix == 2, "restarted parse ends");
ArgumentVector permuted("foo a -b c");
ExpectText(RunGetopt("bc", permuted), "|3", "getopt does not permute");
}
void LongOptions() {
int flag = 0;
const GuestOption options[] = {
{"required", 1, nullptr, 'r'},
{"optional", 2, nullptr, 'o'},
{"none", 0, nullptr, 'n'},
{"verbose", 0, nullptr, 'v'},
{"version", 0, nullptr, 'V'},
{"flag", 0, &flag, 7},
{nullptr, 0, nullptr, 0},
};
ArgumentVector kinds("foo --required=1 --required 2 --optional=3 --optional 4 --none --flag");
ExpectText(RunGetoptLong("", options, kinds), "-required=1,-required=2,-optional=3,-optional,-none,-flag|1", "three argument kinds");
Expect(flag == 7, "flag pointer set");
ExpectText(kinds.Joined(), "foo --required=1 --required 2 --optional=3 --optional --none --flag 4", "operand permuted to the end");
ArgumentVector abbreviations("foo --verb --vers --req=x --opt");
ExpectText(RunGetoptLong("", options, abbreviations), "-verbose,-version,-required=x,-optional|0", "unique abbreviations");
struct ErrorCase {
const char* options;
const char* arguments;
int result;
int optopt;
const char* message;
};
const ErrorCase errors[] = {
{"", "foo --ver", '?', 0, "eboot.bin: option `--ver' is ambiguous\n"},
{"", "foo --none=1", '?', 'n', "eboot.bin: option `--none' doesn't allow an argument\n"},
{"", "foo --flag=1", '?', 0, "eboot.bin: option `--flag' doesn't allow an argument\n"},
{"", "foo --required", '?', 'r', "eboot.bin: option `--required' requires an argument\n"},
{":", "foo --required", ':', 'r', ""},
{"", "foo --nope=1", '?', 0, "eboot.bin: unrecognized option `--nope=1'\n"},
{"", "foo -x", '?', 'x', "eboot.bin: invalid option -- x\n"},
{"c:", "foo -c", '?', 'c', "eboot.bin: option requires an argument -- c\n"},
{":c:", "foo -c", ':', 'c', ""},
{"W;", "foo -W ver", '?', 0, "eboot.bin: option `-W ver' is ambiguous\n"},
{"W;", "foo -W", '?', 'W', "eboot.bin: option requires an argument -- W\n"},
};
for (const auto& test : errors) {
ArgumentVector arguments(test.arguments);
StderrCapture capture;
Reset();
optopt_nid_postfix = -1;
const int result = getopt_long_nid_postfix(arguments.Count(), arguments.Values(), test.options, options, nullptr);
Expect(result == test.result, std::string("getopt_long result ") + test.arguments);
Expect(optopt_nid_postfix == test.optopt, std::string("getopt_long optopt ") + test.arguments);
ExpectText(capture.Text(), test.message, std::string("getopt_long message ") + test.arguments);
}
{
ArgumentVector arguments("foo --ver -x");
StderrCapture capture;
Reset();
opterr_nid_postfix = 0;
Expect(getopt_long_nid_postfix(arguments.Count(), arguments.Values(), "", options, nullptr) == '?' && optopt_nid_postfix == 0, "silent ambiguity");
Expect(getopt_long_nid_postfix(arguments.Count(), arguments.Values(), "", options, nullptr) == '?' && optopt_nid_postfix == 'x', "silent unknown");
ExpectText(capture.Text(), "", "opterr 0 silences getopt_long");
}
ArgumentVector wOption("foo -W verbose -Wversion");
ExpectText(RunGetoptLong("W;", options, wOption), "-verbose,-version|0", "W; long options");
ArgumentVector terminated("foo a -b -- -d c");
ExpectText(RunGetoptLong("bd", options, terminated), "b|3", "-- ends getopt_long");
ExpectText(terminated.Joined(), "foo -b -- a -d c", "-- permutes skipped operands");
ArgumentVector interleaved("foo a -b c -d");
ExpectText(RunGetoptLong("bd", options, interleaved), "b,d|2", "permutation");
ExpectText(interleaved.Joined(), "foo -b -d a c", "permuted order");
ArgumentVector plus("foo a -b");
ExpectText(RunGetoptLong("+b", options, plus), "|2", "+ stops at the first operand");
ArgumentVector inOrder("foo a -b c");
Reset();
std::string sequence;
for (int character; (character = getopt_long_nid_postfix(inOrder.Count(), inOrder.Values(), "-b", options, nullptr)) != -1;)
sequence += character == 1 ? std::string("1:") + optarg_nid_postfix + "," : std::string(1, static_cast<char>(character)) + ",";
ExpectText(sequence, "1:a,b,1:c,", "- returns operands in order");
ArgumentVector restartFirst("foo -bd");
ArgumentVector restartSecond("bar -b");
Reset();
Expect(getopt_long_nid_postfix(restartFirst.Count(), restartFirst.Values(), "bd", options, nullptr) == 'b', "getopt_long first parse");
optind_nid_postfix = 0;
Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == 'b', "optind 0 restarts getopt_long");
Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == -1 && optind_nid_postfix == 2, "restarted getopt_long ends");
Reset();
Expect(getopt_long_nid_postfix(restartFirst.Count(), restartFirst.Values(), "bd", options, nullptr) == 'b', "getopt_long parse before optreset");
optind_nid_postfix = optreset_nid_postfix = 1;
Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == 'b', "optreset restarts getopt_long");
Expect(setenv_nid_postfix("POSIXLY_CORRECT", "1", 1) == 0, "setenv POSIXLY_CORRECT");
{
ArgumentVector posix("foo a -b");
ExpectText(RunGetoptLong("b", options, posix), "|2", "POSIXLY_CORRECT stops at the first operand");
ArgumentVector illegal("foo -x");
StderrCapture capture;
Reset();
Expect(getopt_long_nid_postfix(illegal.Count(), illegal.Values(), "b", options, nullptr) == '?', "POSIXLY_CORRECT unknown option");
ExpectText(capture.Text(), "eboot.bin: illegal option -- x\n", "POSIXLY_CORRECT message");
}
Expect(unsetenv_nid_postfix("POSIXLY_CORRECT") == 0, "unsetenv POSIXLY_CORRECT");
ArgumentVector gnu("foo a -b");
ExpectText(RunGetoptLong("b", options, gnu), "b|1", "permutation after POSIXLY_CORRECT is unset");
Reset();
Expect(getopt_long_nid_postfix(gnu.Count(), gnu.Values(), nullptr, options, nullptr) == -1, "null option string");
}
}
int main() {
Expect(opterr_nid_postfix == 1 && optind_nid_postfix == 1 && optopt_nid_postfix == 0 &&
optreset_nid_postfix == 0 && optarg_nid_postfix == nullptr, "initial values");
FreeBsdGetoptCases();
FreeBsdGetoptLongCases();
ShortOptions();
LongOptions();
}