mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
The #1653 snprintf ban converted mux_nls.cpp's path build to mux_sprintf -- correct in itself -- and test_plural.cpp compiles that file standalone to reach the anonymous-namespace plural_eval(), linking nothing. Undefined reference; `make test` red at test-nls-plural on master for everyone. Supply the symbol in the test, vsnprintf-backed, with the honesty note in place: the only caller is mux_nls_init()'s catalogue-path build, which no plural test exercises -- the linker merely needs the definition. If a tested path ever formats through the stub, the note says to stop stubbing and link the real one, because mux_sprintf differs from vsnprintf at UTF-8 truncation boundaries. Same shape as the established recipe for unit-testing library TUs with a handful of stubs (tests/dbt, netaddr). Verified: make test-nls-plural 43 passed, 0 failed. Found while validating #1745 on this box -- its TESTEXIT=2 here was this master breakage, not the lua_jit flip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
199 lines
6.6 KiB
C++
199 lines
6.6 KiB
C++
/*! \file test_plural.cpp
|
|
* \brief Unit test for the built-in reader's Plural-Forms evaluator (#1702).
|
|
*
|
|
* Why this exists as a unit test rather than a server scenario:
|
|
*
|
|
* #1702 made the built-in MO reader the only catalogue path on every
|
|
* platform, which meant implementing Plural-Forms here instead of borrowing
|
|
* ngettext(3) from libintl. That moved plural selection for EVERY platform
|
|
* onto one expression evaluator -- so a bug in it is no longer a Windows
|
|
* bug, it is everybody's.
|
|
*
|
|
* tests/nls/run.sh drives plurals through the server, but it can only reach
|
|
* the counts a scenario can produce (@entrances with one or two exits) and
|
|
* only the rule the shipped catalogues carry (nplurals=2, plural=(n!=1), and
|
|
* ko's nplurals=1). Neither exercises ternary chaining, %, && / ||, or the
|
|
* out-of-range and malformed cases. Russian needs n=21 to be interesting,
|
|
* and digging 21 exits to test an expression parser is the wrong shape.
|
|
*
|
|
* Reaching plural_eval() means being in its translation unit, since it lives
|
|
* in an anonymous namespace -- the same reason tests/dbt/test_interp.cpp
|
|
* includes dbt_interp.cpp to reach its file-static mem_check.
|
|
*/
|
|
|
|
#define HAVE_NLS 1
|
|
#include "mux_nls.cpp"
|
|
|
|
// mux_nls.cpp calls mux_sprintf (the #1653 snprintf ban), which lives in
|
|
// libmux -- and this test deliberately links nothing, because reaching the
|
|
// anonymous-namespace plural_eval() means being in mux_nls.cpp's own
|
|
// translation unit. So supply the symbol here, vsnprintf-backed.
|
|
//
|
|
// Faithful for this test's purposes: the only mux_sprintf caller in
|
|
// mux_nls.cpp is the catalogue-path build in mux_nls_init(), which no test
|
|
// below exercises -- the linker merely needs the definition. The real
|
|
// mux_sprintf differs from vsnprintf at UTF-8 truncation boundaries; if a
|
|
// tested path ever starts formatting player-visible text through this stub,
|
|
// stop stubbing and link the real one.
|
|
//
|
|
#include <cstdarg>
|
|
extern "C++" void DCL_CDECL mux_sprintf(UTF8 *buff, size_t count,
|
|
const UTF8 *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(reinterpret_cast<char *>(buff), count,
|
|
reinterpret_cast<const char *>(fmt), ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
namespace
|
|
{
|
|
int g_pass = 0;
|
|
int g_fail = 0;
|
|
|
|
// Point the evaluator at a rule, as plural_load() would after reading a
|
|
// catalogue header.
|
|
//
|
|
void use_rule(unsigned long nplurals, const char *expr)
|
|
{
|
|
s_nplurals = nplurals;
|
|
if (nullptr == expr)
|
|
{
|
|
s_plural_expr = nullptr;
|
|
return;
|
|
}
|
|
snprintf(s_plural_buf, sizeof(s_plural_buf), "%s", expr);
|
|
s_plural_expr = s_plural_buf;
|
|
}
|
|
|
|
void expect(const char *label, unsigned long n, unsigned long want)
|
|
{
|
|
const unsigned long got = plural_eval(n);
|
|
if (got == want)
|
|
{
|
|
g_pass++;
|
|
}
|
|
else
|
|
{
|
|
g_fail++;
|
|
printf("not ok - %s: n=%lu wanted form %lu, got %lu\n",
|
|
label, n, want, got);
|
|
}
|
|
}
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
// ---- English: the .pot's own rule, and the fallback shape ----
|
|
use_rule(2, "(n != 1)");
|
|
expect("en", 0, 1);
|
|
expect("en", 1, 0);
|
|
expect("en", 2, 1);
|
|
expect("en", 21, 1);
|
|
|
|
// ---- No rule at all: must behave as English, not as form 0 ----
|
|
//
|
|
// An untranslated or header-less catalogue still has to pick sensibly,
|
|
// and "always singular" would render "1 entrances found" for every count.
|
|
//
|
|
use_rule(2, nullptr);
|
|
expect("no-rule", 1, 0);
|
|
expect("no-rule", 2, 1);
|
|
|
|
// ---- Korean / Japanese / Chinese: one form, always ----
|
|
//
|
|
// This is the case the old built-in reader got wrong in the other
|
|
// direction: it returned the English plural for n>1, so ko saw a form
|
|
// its catalogue does not even define.
|
|
//
|
|
use_rule(1, "0");
|
|
expect("ko", 1, 0);
|
|
expect("ko", 2, 0);
|
|
expect("ko", 11, 0);
|
|
|
|
// ---- French: plural starts at 2, and 0 is singular ----
|
|
use_rule(2, "(n > 1)");
|
|
expect("fr", 0, 0);
|
|
expect("fr", 1, 0);
|
|
expect("fr", 2, 1);
|
|
|
|
// ---- Russian: three forms, chained ternaries, % and && and || ----
|
|
//
|
|
// The rule this evaluator exists for. Hand-checked against the standard
|
|
// ru Plural-Forms: form 0 for 1/21/31 (but not 11), form 1 for 2-4/22-24
|
|
// (but not 12-14), form 2 otherwise.
|
|
//
|
|
use_rule(3,
|
|
"(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 "
|
|
"&& (n%100<10 || n%100>=20) ? 1 : 2)");
|
|
expect("ru", 1, 0);
|
|
expect("ru", 2, 1);
|
|
expect("ru", 4, 1);
|
|
expect("ru", 5, 2);
|
|
expect("ru", 11, 2); // 11 is the exception to the n%10==1 rule
|
|
expect("ru", 12, 2); // 12-14 are the exception to the 2-4 rule
|
|
expect("ru", 14, 2);
|
|
expect("ru", 21, 0);
|
|
expect("ru", 22, 1);
|
|
expect("ru", 25, 2);
|
|
expect("ru", 101, 0);
|
|
expect("ru", 111, 2);
|
|
expect("ru", 0, 2);
|
|
|
|
// ---- Polish: a different three-form split, to catch a rule fitted
|
|
// to Russian by accident ----
|
|
use_rule(3,
|
|
"(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)");
|
|
expect("pl", 1, 0);
|
|
expect("pl", 2, 1);
|
|
expect("pl", 5, 2);
|
|
expect("pl", 21, 2); // unlike ru, 21 is NOT form 0 in pl
|
|
expect("pl", 22, 1);
|
|
|
|
// ---- Arabic: six forms, the widest rule in common use ----
|
|
use_rule(6,
|
|
"(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 "
|
|
": n%100>=11 ? 4 : 5)");
|
|
expect("ar", 0, 0);
|
|
expect("ar", 1, 1);
|
|
expect("ar", 2, 2);
|
|
expect("ar", 5, 3);
|
|
expect("ar", 15, 4);
|
|
expect("ar", 102, 5);
|
|
|
|
// ---- Hostile and malformed input: must be total, never trap ----
|
|
//
|
|
// A .mo is data the operator may not have written. Every one of these
|
|
// has to yield some form rather than crash or read past the buffer.
|
|
//
|
|
use_rule(2, "n/0");
|
|
expect("div-by-zero", 5, 0);
|
|
|
|
use_rule(2, "n%0");
|
|
expect("mod-by-zero", 5, 0);
|
|
|
|
use_rule(2, "(n"); // unbalanced
|
|
expect("unbalanced", 1, 0);
|
|
|
|
use_rule(2, "n ? 1"); // ternary with no ':'
|
|
expect("truncated-ternary", 1, 0);
|
|
|
|
use_rule(2, "@@@"); // not the grammar at all
|
|
expect("garbage", 1, 0);
|
|
|
|
use_rule(2, ""); // empty expression
|
|
expect("empty", 1, 0);
|
|
|
|
// A rule selecting a form the catalogue does not carry must clamp to 0
|
|
// rather than index off the end of the translation entry.
|
|
//
|
|
use_rule(2, "5");
|
|
expect("out-of-range", 1, 0);
|
|
|
|
printf("=== tests/nls plural: %d passed, %d failed ===\n", g_pass, g_fail);
|
|
return (0 == g_fail) ? 0 : 1;
|
|
}
|