test.hh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. // -*- mode: c++; coding: utf-8 -*-
  2. // ra-ra - Test/benchmarking library.
  3. // (c) Daniel Llorens - 2012-2025
  4. // This library is free software; you can redisribute it and/or modify it under
  5. // the terms of the GNU Lesser General Public License as published by the Free
  6. // Software Foundation; either version 3 of the License, or (at your option) any
  7. // later version.
  8. #pragma once
  9. #include <string>
  10. #include <iomanip>
  11. #include <iostream>
  12. #include <chrono>
  13. #include <ctime>
  14. #include "ra.hh"
  15. namespace ra {
  16. namespace esc {
  17. constexpr char const * bold = "\x1b[01m";
  18. constexpr char const * unbold = "\x1b[0m";
  19. constexpr char const * invert = "\x1b[07m";
  20. constexpr char const * underline = "\x1b[04m";
  21. constexpr char const * red = "\x1b[31m";
  22. constexpr char const * green = "\x1b[32m";
  23. constexpr char const * cyan = "\x1b[36m";
  24. constexpr char const * yellow = "\x1b[33m";
  25. constexpr char const * blue = "\x1b[34m";
  26. constexpr char const * white = "\x1b[97m"; // AIXTERM
  27. constexpr char const * plain = "\x1b[39m";
  28. constexpr char const * reset = "\x1b[39m\x1b[0m"; // plain + unbold
  29. constexpr char const * pink = "\x1b[38;5;225m";
  30. } // namespace esc
  31. struct TestRecorder
  32. {
  33. constexpr static double QNAN = std::numeric_limits<double>::quiet_NaN();
  34. constexpr static double PINF = std::numeric_limits<double>::infinity();
  35. // ra::amax ignores nans like fmax does, we don't want that here.
  36. __attribute__((optimize("-fno-finite-math-only")))
  37. static auto
  38. amax_strict(auto const & a)
  39. {
  40. using T = ncvalue_t<decltype(a)>;
  41. T c = std::numeric_limits<T>::has_infinity ? -std::numeric_limits<T>::infinity() : std::numeric_limits<T>::lowest();
  42. return early(map([&c](auto const & a) { if (c<a) c=a; return isnan(a) ? std::make_optional(QNAN*a) : std::nullopt; }, a), c);
  43. }
  44. enum verbose_t { QUIET, // as NOISY if failed, else no output
  45. ERRORS, // as NOISY if failed, else info and fp errors (default)
  46. NOISY }; // full output of info, test arguments, fp errors
  47. std::ostream & o;
  48. verbose_t verbose_default, verbose;
  49. bool willskip=false, willexpectfail=false, willstrictshape=false;
  50. int total=0, skipped=0, passed_good=0, passed_bad=0, failed_good=0, failed_bad=0;
  51. std::vector<int> bad;
  52. std::string infos;
  53. TestRecorder(std::ostream & o_=std::cout, verbose_t verbose_default_=ERRORS)
  54. : o(o_), verbose_default(verbose_default_), verbose(verbose_default_) {}
  55. void
  56. section(auto const & ... a)
  57. {
  58. print(o, "\n", esc::bold, a ..., esc::unbold, "\n");
  59. }
  60. static std::string
  61. format_error(double e)
  62. {
  63. return std::format("{}{:.2}{}", esc::yellow, e, esc::reset);
  64. }
  65. TestRecorder &
  66. info(auto && ... a)
  67. {
  68. bool empty = (infos=="");
  69. infos += format(esc::pink, (empty ? "" : "; "), a ..., esc::reset);
  70. return *this;
  71. }
  72. TestRecorder & quiet(verbose_t v=QUIET) { verbose = v; return *this; }
  73. TestRecorder & noisy(verbose_t v=NOISY) { verbose = v; return *this; }
  74. TestRecorder & skip(bool s=true) { willskip = s; return *this; }
  75. TestRecorder & strictshape(bool s=true) { willstrictshape = s; return *this; }
  76. TestRecorder & expectfail(bool s=true) { willexpectfail = s; return *this; }
  77. #define RA_CURRENT_LOC std::source_location const loc = std::source_location::current()
  78. #define RA_LAZYINFO(...) [&] { return format(infos, (infos=="" ? "" : "; "), __VA_ARGS__); }
  79. void
  80. test(bool c, auto && info_full, auto && info_min, RA_CURRENT_LOC)
  81. {
  82. switch (verbose) {
  83. case QUIET: {
  84. if (!c) {
  85. print(o, esc::cyan, total, ":", loc, "]", esc::reset, " ...", esc::bold, esc::red, " FAIL", esc::reset,
  86. esc::yellow, (willskip ? " skipped" : ""), (willexpectfail ? " expected" : ""), esc::reset,
  87. " ", info_full(), "\n");
  88. }
  89. }; break;
  90. case NOISY: case ERRORS: {
  91. print(o, esc::cyan, "[", total, ":", loc, "]", esc::reset, " ...",
  92. (c ? std::string(esc::green) + " ok" + esc::reset
  93. : std::string(esc::bold) + esc::red + " FAIL" + esc::reset),
  94. esc::yellow, (willskip ? " skipped" : ""),
  95. (willexpectfail ? (c ? " not expected" : " expected") : ""), esc::reset,
  96. " ", (verbose==NOISY || c==willexpectfail ? info_full() : info_min()), "\n");
  97. }; break;
  98. default: std::abort();
  99. }
  100. infos = "";
  101. verbose = verbose_default;
  102. if (!willskip) {
  103. ++(willexpectfail? (c ? passed_bad : failed_good) : (c ? passed_good : failed_bad));
  104. if (c==willexpectfail) {
  105. bad.push_back(total);
  106. }
  107. } else {
  108. ++skipped;
  109. }
  110. ++total;
  111. willstrictshape = willskip = willexpectfail = false;
  112. }
  113. void
  114. test(bool c, auto && info_full, RA_CURRENT_LOC)
  115. {
  116. test(c, info_full, info_full, loc);
  117. }
  118. void
  119. test(bool c, RA_CURRENT_LOC)
  120. {
  121. test(c, RA_LAZYINFO(""), loc);
  122. }
  123. bool
  124. test_scomp(auto && a, auto && b, auto && comp, char const * msg, RA_CURRENT_LOC)
  125. {
  126. bool c = comp(a, b);
  127. test(c, RA_LAZYINFO(b, " (", msg, " ", a, ")"), RA_LAZYINFO(""), loc);
  128. return c;
  129. }
  130. bool
  131. test_seq(auto && ref, auto && a, RA_CURRENT_LOC)
  132. {
  133. return test_scomp(ref, a, [](auto && a, auto && b) { return a==b; }, "should be strictly ==", loc);
  134. }
  135. // where() is used to match shapes if either REF or A don't't have one.
  136. bool
  137. test_comp(auto && a, auto && b, auto && comp, char const * msg, RA_CURRENT_LOC)
  138. {
  139. if (willstrictshape
  140. ? [&] {
  141. if constexpr (ra::rank_s(a)==ra::rank_s(b) || ra::rank_s(a)==ANY || ra::rank_s(b)==ANY) {
  142. return ra::rank(a)==ra::rank(b) && every(ra::start(ra::shape(a))==ra::shape(b));
  143. } else {
  144. return false;
  145. } }()
  146. : agree_op(comp, a, b)) {
  147. bool c = every(ra::map(comp, a, b));
  148. test(c,
  149. RA_LAZYINFO(where(false, a, b), " (", where(true, a, b), " ", msg, ")"),
  150. RA_LAZYINFO(""),
  151. loc);
  152. return c;
  153. } else {
  154. test(false,
  155. RA_LAZYINFO("Mismatched shapes [", fmt(nstyle, ra::shape(a)), "] [", fmt(nstyle, ra::shape(b)), "]",
  156. willstrictshape ? " (strict shape)" : ""),
  157. RA_LAZYINFO("Mismatched shapes", willstrictshape ? " (strict shape)" : ""),
  158. loc);
  159. return false;
  160. }
  161. }
  162. #define RA_TEST_COMP(NAME, OP) \
  163. bool \
  164. JOIN(test_, NAME)(auto && ref, auto && a, RA_CURRENT_LOC) \
  165. { \
  166. return test_comp(ra::start(ref), ra::start(a), [](auto && a, auto && b) { return every(a OP b); }, \
  167. "should be " STRINGIZE(OP), loc); \
  168. }
  169. RA_TEST_COMP(eq, ==)
  170. RA_TEST_COMP(lt, <)
  171. RA_TEST_COMP(le, <=)
  172. RA_TEST_COMP(gt, >)
  173. RA_TEST_COMP(ge, >=)
  174. #undef RA_TEST_COMP
  175. __attribute__((optimize("-fno-finite-math-only")))
  176. double
  177. test_rel(auto && ref_, auto && a_, double req, double level=0, RA_CURRENT_LOC)
  178. {
  179. decltype(auto) ref = ra::start(ref_);
  180. decltype(auto) a = ra::start(a_);
  181. double e = (level<=0)
  182. ? amax_strict(where(isfinite(ref),
  183. rel_error(ref, a),
  184. where(isinf(ref),
  185. where(ref==a, 0., PINF),
  186. where(isnan(a), 0., PINF))))
  187. : amax_strict(where(isfinite(ref),
  188. abs(ref-a)/level,
  189. where(isinf(ref),
  190. where(ref==a, 0., PINF),
  191. where(isnan(a), 0., PINF))));
  192. test(e<=req,
  193. RA_LAZYINFO("rerr (", esc::yellow, "ref", esc::reset, ": ", ref, esc::yellow, ", got", esc::reset, ": ", a,
  194. ") = ", format_error(e), (level<=0 ? "" : format(" (level ", level, ")")), ", req. ", req),
  195. RA_LAZYINFO("rerr: ", format_error(e), (level<=0 ? "" : format(" (level ", level, ")")),
  196. ", req. ", req),
  197. loc);
  198. return e;
  199. }
  200. __attribute__((optimize("-fno-finite-math-only")))
  201. double
  202. test_abs(auto && ref_, auto && a_, double req=0, RA_CURRENT_LOC)
  203. {
  204. decltype(auto) ref = ra::start(ref_);
  205. decltype(auto) a = ra::start(a_);
  206. double e = amax_strict(where(isfinite(ref),
  207. abs(ref-a),
  208. where(isinf(ref),
  209. where(ref==a, 0., PINF),
  210. where(isnan(a), 0., PINF))));
  211. test(e<=req,
  212. RA_LAZYINFO("aerr (ref: ", ref, ", got: ", a, ") = ", format_error(e), ", req. ", req),
  213. RA_LAZYINFO("aerr: ", format_error(e), ", req. ", req),
  214. loc);
  215. return e;
  216. }
  217. #undef RA_CURRENT_LOC
  218. #undef RA_LAZYINFO
  219. int
  220. summary() const
  221. {
  222. std::time_t t = std::time(nullptr);
  223. tm * tmp = std::localtime(&t);
  224. char buf[64];
  225. std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tmp);
  226. print(o, "--------------\nTests end ", buf, ". ",
  227. "Of ", total, " tests passed ", (passed_good+passed_bad),
  228. " (", passed_bad, " unexpected), failed ", (failed_good+failed_bad),
  229. " (", failed_bad, " unexpected), skipped ", skipped, ".\n");
  230. if (bad.size()>0) {
  231. print(o, bad.size(), " bad tests: [", esc::bold, esc::red, fmt(nstyle, bad), esc::reset, "].\n");
  232. }
  233. return bad.size();
  234. }
  235. };
  236. // TODO measure empty loops, better reporting. Let benchmarked functions return results
  237. struct Benchmark
  238. {
  239. using clock = std::conditional_t<std::chrono::high_resolution_clock::is_steady,
  240. std::chrono::high_resolution_clock,
  241. std::chrono::steady_clock>;
  242. static clock::duration
  243. lapse(clock::duration empty, clock::duration full)
  244. {
  245. return (full>empty) ? full-empty : full;
  246. }
  247. static double
  248. toseconds(clock::duration const & t)
  249. {
  250. return std::chrono::duration<float, std::ratio<1, 1>>(t).count();
  251. }
  252. struct Value
  253. {
  254. std::string name;
  255. int repeats;
  256. clock::duration empty;
  257. ra::Big<clock::duration, 1> times;
  258. };
  259. static double
  260. avg(Value const & bv)
  261. {
  262. return toseconds(sum(bv.times))/bv.repeats/bv.times.size();
  263. }
  264. static double
  265. stddev(Value const & bv)
  266. {
  267. return sqrt(sum(sqr(ra::map(toseconds, bv.times)/bv.repeats-avg(bv)))/bv.times.size());
  268. }
  269. static std::string
  270. report(Value const & bv, double scale=1., double u=1e-9)
  271. {
  272. return std::format("{0:3f} {2} [{1:3f}]", avg(bv)/scale/u, stddev(bv)/scale/u,
  273. 1e-9==u ? "ns" : 1e-6==u ? "us" : 1e-3==u ? "ms" : 1==u ? "s" : "?");
  274. }
  275. void
  276. report(std::ostream & o, auto const & b, double frac)
  277. {
  278. std::println(o, "{}{::4.2}", (infos=="" ? "" : infos + " : "), start(ra::map(avg, b)/frac));
  279. std::println(o, "{}{::4.2}", (infos=="" ? "" : infos + " : "), start(ra::map(stddev, b)/frac));
  280. infos = "";
  281. }
  282. int repeats_ = 1, runs_ = 1;
  283. std::string const name_ = "";
  284. std::string infos = "";
  285. Benchmark &
  286. info(auto && ... a)
  287. {
  288. bool empty = (infos=="");
  289. infos += format(ra::esc::plain, (empty ? "" : "; "), a ..., ra::esc::plain);
  290. return *this;
  291. }
  292. Benchmark name(std::string name_) { return Benchmark { repeats_, runs_, name_, "" }; }
  293. Benchmark repeats(int repeats_) { return Benchmark { repeats_, runs_, name_, "" }; }
  294. Benchmark runs(int runs_) { return Benchmark { repeats_, runs_, name_, "" }; }
  295. auto
  296. once(auto && f, auto && ... a)
  297. {
  298. auto t0 = clock::now();
  299. clock::duration empty = clock::now()-t0;
  300. ra::Big<clock::duration, 1> times;
  301. for (int k=0; k<runs_; ++k) {
  302. auto t0 = clock::now();
  303. for (int i=0; i<repeats_; ++i) {
  304. f(RA_FWD(a) ...);
  305. }
  306. clock::duration full = clock::now()-t0;
  307. times.push_back(lapse(empty, full));
  308. }
  309. return Value { name_, repeats_, empty, std::move(times) };
  310. }
  311. auto
  312. once_f(auto && g, auto && ... a)
  313. {
  314. clock::duration empty;
  315. g([&](auto && f)
  316. {
  317. auto t0 = clock::now();
  318. empty = clock::now()-t0;
  319. }, RA_FWD(a) ...);
  320. ra::Big<clock::duration, 1> times;
  321. for (int k=0; k<runs_; ++k) {
  322. g([&](auto && f)
  323. {
  324. auto t0 = clock::now();
  325. for (int i=0; i<repeats_; ++i) { f(); }
  326. clock::duration full = clock::now()-t0;
  327. times.push_back(lapse(empty, full));
  328. }, RA_FWD(a) ...);
  329. }
  330. return Value { name_, repeats_, empty, std::move(times) };
  331. }
  332. auto
  333. run(auto && f, auto && ... a)
  334. {
  335. return ra::concrete(ra::from([this, &f](auto && ... b) { return this->once(f, b ...); }, a ...));
  336. }
  337. auto
  338. run_f(auto && f, auto && ... a)
  339. {
  340. return ra::concrete(ra::from([this, &f](auto && ... b) { return this->once_f(f, b ...); }, a ...));
  341. }
  342. };
  343. template <> constexpr bool is_scalar_def<Benchmark::Value> = true;
  344. } // namespace ra