File: ldml_test_source.cpp

package info (click to toggle)
keyman 18.0.246-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 21,316 kB
  • sloc: python: 52,784; cpp: 21,289; sh: 7,633; ansic: 4,823; xml: 3,617; perl: 959; makefile: 139; javascript: 138
file content (890 lines) | stat: -rw-r--r-- 27,130 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <iterator>
#include <list>
#include <sstream>
#include <string>
#include <type_traits>

#if 0
// TODO-LDML If we need to avoid exceptions in JSON
#define JSON_TRY_USER if(true)
#define JSON_CATCH_USER(exception) if(false)
#define JSON_THROW_USER(exception) { return __LINE__; } // get out
#endif

#include <json.hpp>

// Ensure that ICU gets included even on wasm.
#define KMN_IN_LDML_TESTS

#include <kmx/kmx_processevent.h> // for char to vk mapping tables
#include <kmx/kmx_xstring.h> // for surrogate pair macros
#include <kmx/kmx_plus.h>
#include "ldml/keyman_core_ldml.h"
#include "ldml/ldml_processor.hpp"
#include "ldml/ldml_markers.hpp"

#include "path.hpp"
#include "state.hpp"
#include "utfcodec.hpp"
#include "util_normalize.hpp"

#include "ldml_test_source.hpp"
#include "ldml_test_utils.hpp"

#include "core_icu.h"
#include "unicode/uniset.h"
#include "unicode/usetiter.h"

#include "../load_kmx_file.hpp"

#include <test_assert.h>
#include <test_color.h>

#define assert_or_return(expr) if(!(expr)) { \
  std::wcerr << __FILE__ << ":" << __LINE__ << ": " << \
  console_color::fg(console_color::BRIGHT_RED) \
             << "warning: " << (#expr) \
             << console_color::reset() \
             << std::endl; \
  return __LINE__; \
}

#define TEST_JSON_SUFFIX "-test.json"
namespace km {
namespace tests {


/** string munging */
static void append_to_str(std::u16string &str, const char *buf) {
  const PKMX_WCHAR p = km::core::kmx::strtowstr((char *)buf); /** cast away const, unused*/
  const std::u16string p2(p);
  str.append(p2);
  delete [] p;
}

/** string munging */
static void append_to_str(std::u16string &str, long n) {
  char buf[64];

  snprintf(buf, 64, "%ld", n);
  append_to_str(str, buf);
}

void
ldml_action::formatType(const char *f, int l, ldml_action_type setType, const std::u16string &msg) {
  type = setType;
  string.clear();
  append_to_str(string, f);
  string.append(u":");
  append_to_str(string, l);
  string.append(u" ");
  string.append(msg);
}

void
ldml_action::formatType(const char *f, int l, ldml_action_type setType, const std::u16string &msg, const std::u16string &msg2) {
  std::u16string tmp = msg;
  tmp.append(msg2);
  formatType(f, l, setType, tmp);
}

void
ldml_action::formatType(const char *f, int l, ldml_action_type setType, const std::u16string &msg, long msg2) {
  std::u16string tmp;
  append_to_str(tmp, msg2);
  formatType(f, l, setType, msg, tmp);
}

void
ldml_action::formatType(const char *f, int l, ldml_action_type setType, const std::u16string &msg, const std::string &msg2) {
  std::u16string tmp;
  append_to_str(tmp, msg2.c_str());
  formatType(f, l, setType, msg, tmp);
}

bool ldml_action::done() const {
  return (type == LDML_ACTION_DONE || type == LDML_ACTION_SKIP || type == LDML_ACTION_FAIL);
}

LdmlTestSource::LdmlTestSource() {
}


LdmlTestSource::~LdmlTestSource() {

}

km_core_status LdmlTestSource::get_expected_load_status() {
  return KM_CORE_STATUS_OK;
}

bool LdmlTestSource::get_expected_beep() const {
  return false;
}

int LdmlTestSource::load_kmx_plus(const km::core::path &compiled) {
    // check and load the KMX (yes, once again)
  rawdata = km::tests::load_kmx_file(compiled);
  if(!km::core::ldml_processor::is_handled(rawdata)) {
    std::cerr << "Reading KMX for test purposes failed: " << compiled << std::endl;
    return __LINE__;
  }

  auto comp_keyboard = (const km::core::kmx::COMP_KEYBOARD*)rawdata.data();
  // initialize the kmxplus object with our copy
  kmxplus.reset(new km::core::kmx::kmx_plus(comp_keyboard, rawdata.size()));

  if (!kmxplus->is_valid()) {
    std::cerr << "kmx_plus invalid" << std::endl;
    return __LINE__;
  }

  if (!kmxplus->key2Helper.valid()) {
    std::cerr << "kmx_plus invalid" << std::endl;
    return __LINE__;
  }

  return 0; // success
}

bool LdmlTestSource::get_vkey_table(std::set<key_event> &fillin) const {
  if (!kmxplus || !kmxplus->is_valid()) {
    return false; // fail
  }

  // just dump the kmap table
  for (KMX_DWORD kmapIdx = 0; kmapIdx < kmxplus->key2->kmapCount; kmapIdx++) {
    const km::core::kmx::COMP_KMXPLUS_KEYS_KMAP *kmap = kmxplus->key2Helper.getKmap(kmapIdx);
    if (kmap == nullptr) {
      return false;
    }
    if (kmap->vkey > 0xFF) {
      continue; // synthetic key- skip
    }
    fillin.insert(key_event(kmap->vkey, kmap->mod));
  }
  return true;
}

// String trim functions from https://stackoverflow.com/a/217605/1836776
// trim from start (in place)
static inline void
ltrim(std::string &s) {
  s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
}

// trim from end (in place)
static inline void
rtrim(std::string &s) {
  s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
}

// trim from both ends (in place)
static inline void
trim(std::string &s) {
  ltrim(s);
  rtrim(s);
}

LdmlEmbeddedTestSource::LdmlEmbeddedTestSource() {

}

LdmlEmbeddedTestSource::~LdmlEmbeddedTestSource() {

}

std::u16string
LdmlTestSource::parse_source_string(std::string const &s) {
  std::u16string t;
  for (auto p = s.begin(); p != s.end(); p++) {
    if (*p == '\\') {
      p++;
      km_core_usv v;
      bool had_open_curly = false;
      test_assert(p != s.end());
      if (*p == 'u' || *p == 'U') {
        // Unicode value
        p++;
        if (*p == '{') {
          p++;
          test_assert(p != s.end());
          had_open_curly = true;
        }
        size_t n;
        std::string s1 = s.substr(p - s.begin(), 8);
        v              = std::stoul(s1, &n, 16);
        // Allow deadkey_number (U+0001) characters and onward
        test_assert(v >= 0x0001 && v <= 0x10FFFF);
        p += n - 1;
        if (v < 0x10000) {
          t += km_core_cu(v);
        } else {
          t += km_core_cu(Uni_UTF32ToSurrogate1(v));
          t += km_core_cu(Uni_UTF32ToSurrogate2(v));
        }
        if (had_open_curly) {
          p++;
          // close what you opened
          test_assert(*p == '}'); // close curly
          test_assert(p != s.end());
        }
      } else if (*p == 'd') {
        // Deadkey
        // TODO, not yet supported
        test_assert(false);
      }
    } else {
      t += *p;
    }
  }
  return t;
}

std::u16string
LdmlTestSource::parse_u8_source_string(std::string const &u8s) {
  // convert from utf-8 to utf-16 first
  std::u16string s = convert<char, char16_t>(u8s);
  std::u16string t;
  for (auto p = s.begin(); p != s.end(); p++) {
    if (*p == '\\') {
      p++;
      km_core_usv v;
      bool had_open_curly = false;
      test_assert(p != s.end());
      if (*p == 'u' || *p == 'U') {
        // Unicode value
        p++;
        if (*p == '{') {
          p++;
          test_assert(p != s.end());
          had_open_curly = true;
        }
        size_t n;
        std::u16string s1 = s.substr(p - s.begin(), 8);
        // TODO-LDML: convert back first?
        std::string s1b = convert<char16_t, char>(s1);
        v              = std::stoul(s1b, &n, 16);
        // Allow deadkey_number (U+0001) characters and onward
        test_assert(v >= 0x0001 && v <= 0x10FFFF);
        p += n - 1;
        if (v < 0x10000) {
          t += km_core_cu(v);
        } else {
          t += km_core_cu(Uni_UTF32ToSurrogate1(v));
          t += km_core_cu(Uni_UTF32ToSurrogate2(v));
        }
        if (had_open_curly) {
          p++;
          // close what you opened
          test_assert(*p == '}'); // close curly
          test_assert(p != s.end());
        }
      } else if (*p == 'd') {
        // Deadkey
        // TODO, not yet supported
        test_assert(false);
      }
    } else {
      t += *p;
    }
  }
  return t;
}

bool
LdmlEmbeddedTestSource::is_token(const std::string token, std::string &line) {
  if (line.compare(0, token.length(), token) == 0) {
    line = line.substr(token.length());
    trim(line);
    return true;
  }
  return false;
}

int
LdmlEmbeddedTestSource::load_source( const km::core::path &path, const km::core::path &compiled ) {
  const std::string s_keys = "@@keys: ";
  const std::string s_expected = "@@expected: ";
  const std::string s_context = "@@context: ";
  const std::string s_capsLock = "@@capsLock: ";
  const std::string s_expecterror = "@@expect-error: ";
  const std::string s_keylist = "@@keylist: ";

  // Parse out the header statements in file.kmn that tell us (a) environment, (b) key sequence, (c) start context, (d) expected
  // result
  std::ifstream kmn(path.native());
  if (!kmn.good()) {
    std::cerr << "could not open file: " << path << std::endl;
    return __LINE__;
  }
  std::string line;
  while (std::getline(kmn, line)) {
    trim(line);

    if (!line.length())
      continue;
    if (line.compare(0, s_keys.length(), s_keys) == 0) {
      auto k = line.substr(s_keys.length());
      trim(k);
      keys.emplace_back(k);
    } else if (is_token(s_expected, line)) {
      if (line == "\\b") {
        expected_beep = true;
      } else {
        // allow multiple expected lines
        expected.emplace_back(parse_source_string(line));
      }
    } else if (is_token(s_expecterror, line)) {
      expected_error = true;
    } else if (is_token(s_context, line)) {
      context = parse_source_string(line);
    } else if (is_token(s_capsLock, line)) {
      set_caps_lock_on(parse_source_string(line).compare(u"1") == 0);
    } else if (is_token(s_keylist, line)) {
      set_keylist(line);
    } else if (line[0] == '@') {
      std::cerr << path << " warning, unknown @-command " << line << std::endl;
    }
  }

  if (keys.empty() && expected.empty() && !expected_error) {
    // don't note this, the parent will complain if there's neither json nor embedded
    return __LINE__;
  } else if (keys.empty()) {
    // We must at least have a key sequence to run the test
    std::cerr << "Need at least one key sequence." << std::endl;
    return __LINE__;
  } else if(!expected_error && (keys.size() != expected.size())) {
    std::cerr << "Need the same number of " << s_keys << " and " << s_expected << " lines." << std::endl;
    return __LINE__;
  }

  // and load KMX+ for keylist
  if (!expected_error) {
    // don't attempt to load KMX+ on expected error
    return load_kmx_plus(compiled);
  } else {
    return 0;
  }
}

km_core_status
LdmlEmbeddedTestSource::get_expected_load_status() {
  return expected_error ? KM_CORE_STATUS_INVALID_KEYBOARD : KM_CORE_STATUS_OK;
}

const std::u16string&
LdmlEmbeddedTestSource::get_context() {
  return context;
}

bool LdmlEmbeddedTestSource::get_expected_beep() const {
  return expected_beep;
}

int
LdmlTestSource::caps_lock_state() {
  return _caps_lock_on ? KM_CORE_MODIFIER_CAPS : 0;
}

void
LdmlTestSource::toggle_caps_lock_state() {
  _caps_lock_on = !_caps_lock_on;
}

void
LdmlTestSource::set_caps_lock_on(bool caps_lock_on) {
  _caps_lock_on = caps_lock_on;
}

key_event
LdmlTestSource::char_to_event(char ch) {
  test_assert(ch >= 32);
  return {
      km::core::kmx::s_char_to_vkey[(int)ch - 32].vk,
      (uint16_t)(km::core::kmx::s_char_to_vkey[(int)ch - 32].shifted ? KM_CORE_MODIFIER_SHIFT : 0)};
}

uint16_t
LdmlTestSource::get_modifier(std::string const &m) {
  for (int i = 0; km::core::kmx::s_modifier_names[i].name; i++) {
    if (m == km::core::kmx::s_modifier_names[i].name) {
      return km::core::kmx::s_modifier_names[i].modifier;
    }
  }
  return 0;
}

std::string key_event::dump() const {
  std::stringstream f;
  f  << "Key: {" << km::core::kmx::Debug_VirtualKey(vk) << ", " << km::core::kmx::Debug_ModifierName(modifier_state) << "}";
  return f.str();
}

key_event
LdmlEmbeddedTestSource::vkey_to_event(std::string const &vk_event) {
  // vkey format is MODIFIER MODIFIER K_NAME
  // std::cout << "VK=" << vk_event << std::endl;

  std::stringstream f(vk_event);
  std::string s;
  uint16_t modifier_state = 0;
  km_core_virtual_key vk   = 0;
  while (std::getline(f, s, ' ')) {
    uint16_t modifier = get_modifier(s);
    if (modifier != 0) {
      modifier_state |= modifier;
    } else {
      vk = get_vk(s);
      if (vk == 0) {
        std::cerr << "Error parsing [" << vk_event << "] - could not find vkey or modifier: " << s << std::endl;
      }
      test_assert(vk != 0);
      break; // only one vkey allowed
    }
  }

  // The string should be empty at this point
  if (std::getline(f, s, ' ')) {
    std::cerr << "Error parsing vkey ["<<vk_event<<"] - excess string after key: " << s << std::endl;
    test_assert(false);
  }
  test_assert(vk != 0);

  return {vk, modifier_state};
}

void
LdmlEmbeddedTestSource::next_action(ldml_action &fillin) {
  if (keys.empty()) {
    // #3 we are almost done, let's run the key check
    if (check_keylist) {
      fillin.type = LDML_ACTION_CHECK_KEYLIST;
      fillin.string = expected_keylist; // could be empty
      check_keylist = false;
    } else {
      fillin.type = LDML_ACTION_DONE;
    }
  } else if(keys[0].empty()) {
    // #2. Then, when we finish a key set, we check the 'expected' at the end of it.
    // Got to the end of a key set. time to check
    fillin.type = LDML_ACTION_CHECK_EXPECTED;
    fillin.string = expected[0]; // copy expected
    expected.pop_front();
    keys.pop_front();
  } else {
    // #1 First, we process each key
    fillin.type = LDML_ACTION_KEY_EVENT;
    fillin.k = next_key();
  }
}

key_event
LdmlEmbeddedTestSource::next_key() {
  // mutate this->keys
  return parse_next_key(keys[0]);
}

key_event
LdmlEmbeddedTestSource::parse_next_key(std::string &keys) {
  // Parse the next element of the string, chop it off, and return it
  // mutates keys
  if (keys.length() == 0)
    return {0, 0};
  char ch = keys[0];
  if (ch == '[') {
    if (keys.length() > 1 && keys[1] == '[') {
      keys.erase(0, 2);
      return char_to_event(ch);
    }
    auto n = keys.find(']');
    test_assert(n != std::string::npos);
    auto vkey = keys.substr(1, n - 1);
    keys.erase(0, n + 1);
    return vkey_to_event(vkey);
  } else {
    keys.erase(0, 1);
    return char_to_event(ch);
  }
}


class LdmlJsonTestSource : public LdmlTestSource {
public:
  LdmlJsonTestSource(const std::string &path);
  virtual ~LdmlJsonTestSource();
  virtual const std::u16string &get_context();
  int load(const nlohmann::json &test, const km::core::path &compiled);
  virtual void next_action(ldml_action &fillin);
private:
  std::string path;
  nlohmann::json data;  // maybe
  std::u16string context;
  std::u16string expected;
  /**
   * Which action are we on?
  */
  std::size_t action_index = -1;
  /** @return false if not found */
  bool set_key_from_id(key_event& k, const std::u16string& id);
  bool loaded_context = false;
  bool check_keys = true;
};

LdmlJsonTestSource::LdmlJsonTestSource(const std::string &path)
:path(path) {

}

LdmlJsonTestSource::~LdmlJsonTestSource() {
}

bool LdmlJsonTestSource::set_key_from_id(key_event& k, const std::u16string& id) {
  k = {0, 0};  // set to a null value at first.

  test_assert(kmxplus != nullptr);
  // lookup the id
  test_assert(kmxplus->key2 != nullptr);

  test_assert(kmxplus->key2Helper.valid());
  // First, find the string
  KMX_DWORD strId = kmxplus->strs->find(id);
  if (strId == 0) {
    return false;
  }

  // OK. Now we can search the keybag
  KMX_DWORD keyIndex = 0; // initialize loop
  auto *key2 = kmxplus->key2Helper.findKeyByStringId(strId, keyIndex);
  if (key2 == nullptr) {
    return false;
  }

  // Now, look for the _first_ candidate vkey match in the kmap.
  for (KMX_DWORD kmapIndex = 0; kmapIndex < kmxplus->key2->kmapCount; kmapIndex++) {
    auto *kmap = kmxplus->key2Helper.getKmap(kmapIndex);
    test_assert(kmap != nullptr);
    if (kmap->key == keyIndex) {
      k = {(km_core_virtual_key)kmap->vkey, (uint16_t)kmap->mod};
      return true;
    }
  }
  // Else, unfound
  return false;
}


void
LdmlJsonTestSource::next_action(ldml_action &fillin) {
  if ((action_index+1) >= data["/actions"_json_pointer].size()) {
    // add check keylist
    if (check_keys) {
      fillin.type = LDML_ACTION_CHECK_KEYLIST;
      fillin.string.clear();
      check_keys = false;
      return;
    }
    // at end, done
    fillin.type = LDML_ACTION_DONE;
    return;
  }

  action_index++;
  auto action   = data["/actions"_json_pointer].at(action_index);
  // load up several common attributes
  auto type     = action["/type"_json_pointer];
  auto result   = action["/result"_json_pointer];
  auto key      = action["/key"_json_pointer];
  auto to       = action["/to"_json_pointer];

  // is it a check event?
  if (type == "check") {
    fillin.type   = LDML_ACTION_CHECK_EXPECTED;
    fillin.string = LdmlTestSource::parse_u8_source_string(result.get<std::string>());
    if (!get_normalization_disabled()) {
      test_assert(km::core::util::normalize_nfd(fillin.string)); // TODO-LDML: will be NFC when core is normalizing to NFC
    }
    return;
  } else if (type == "keystroke") {
    fillin.type   = LDML_ACTION_KEY_EVENT;
    auto keyId = LdmlTestSource::parse_u8_source_string(key.get<std::string>());
    // now, look up the key
    if (!set_key_from_id(fillin.k, keyId)) {
      fillin.formatType(__FILE__, __LINE__, LDML_ACTION_FAIL, u"Could not find key: ", keyId);
    }
    return;
  } else if (type == "emit") {
    fillin.type   = LDML_ACTION_EMIT_STRING;
    fillin.string = LdmlTestSource::parse_u8_source_string(to.get<std::string>());
    if (!get_normalization_disabled()) {
      test_assert(km::core::util::normalize_nfd(fillin.string)); // TODO-LDML: will be NFC when core is normalizing to NFC
    }
    return;
  } else if (type == "backspace") {
    // backspace is handled as a key event
    fillin.type             = LDML_ACTION_KEY_EVENT;
    fillin.k.modifier_state = 0;
    fillin.k.vk             = KM_CORE_VKEY_BKSP;
    return;
  }

  // unhandled, so fail
  fillin.formatType(__FILE__, __LINE__, LDML_ACTION_FAIL, u"Error, unknown/unhandled action: ", (long)type);
  return;
}

const std::u16string &
LdmlJsonTestSource::get_context() {
  // load this on demand so that we can normalize (or not) properly.

  // TODO-LDML: Need an update to json.hpp to use contains()
  // if (data.contains("/startContext"_json_pointer)) {
  if (!loaded_context && data.find("startContext") != data.end()) {
    // only set startContext if present - it's optional.
    auto startContext = data["/startContext/to"_json_pointer];
    context = LdmlTestSource::parse_u8_source_string(startContext);
    if (!get_normalization_disabled()) {
      test_assert(km::core::util::normalize_nfd(context)); // TODO-LDML: should be NFC
    }
  }
  loaded_context = true;
  return context;
}

int LdmlJsonTestSource::load(const nlohmann::json &data, const km::core::path &compiled) {
  this->data        = data;
  // TODO-LDML: validate JSON here?

  // load up the kmx_plus
  return load_kmx_plus(compiled);
}

#if defined(HAVE_ICU4C)
class LdmlJsonRepertoireTestSource : public LdmlTestSource {
public:
  LdmlJsonRepertoireTestSource(const std::string &path );
  virtual ~LdmlJsonRepertoireTestSource();
  virtual const std::u16string &get_context();
  int load(const nlohmann::json &test, const km::core::path &compiled);
  virtual void next_action(ldml_action &fillin);
private:
  std::string path;
  nlohmann::json data;  // maybe
  std::string type;
  std::u16string context;
  std::u16string expected;
  std::string chars;
  std::unique_ptr<icu::UnicodeSet> uset;
  std::unique_ptr<icu::UnicodeSetIterator> iterator;
  bool need_check = false; // set this after each char
};

LdmlJsonRepertoireTestSource::LdmlJsonRepertoireTestSource(const std::string &path)
:path(path) {

}

LdmlJsonRepertoireTestSource::~LdmlJsonRepertoireTestSource() {
}

void
LdmlJsonRepertoireTestSource::next_action(ldml_action &fillin) {
  if (type != "simple") {
    fillin.formatType(__FILE__, __LINE__, LDML_ACTION_SKIP, u"TODO-LDML: Only 'simple' is supported, not ", type);
    return;
  }

  if (!iterator->next()) {
    fillin.type = LDML_ACTION_DONE;
    return;
  }

  if (need_check) {
    need_check = false;
    fillin.type = LDML_ACTION_CHECK_EXPECTED;
    fillin.string = expected;
    return;
  }

  // we have already excluded strings, so just get the codepoint
  const km_core_usv ch = iterator->getCodepoint();

  // as string for debugging.
  // const icu::UnicodeString& str = iterator->getString();

  km::core::kmx::char16_single ch16;
  std::size_t len = km::core::kmx::Utf32CharToUtf16(ch, ch16);
  std::u16string chstr = std::u16string(ch16.ch, len);
  if (!get_normalization_disabled()) {
    test_assert(km::core::util::normalize_nfd(chstr)); // TODO-LDML: will be NFC when core is normalizing to NFC
  }
  // append to expected
  expected.append(chstr);
  need_check = true;

  // ---------------------------------------------------
  // find a key that can emit this string.
  // TODO-LDML: no transforms yet.
  // TODO-LDML: looking for an exact single key for now

  test_assert(kmxplus != nullptr);
  // lookup the id
  test_assert(kmxplus->strs != nullptr);
  test_assert(kmxplus->key2 != nullptr);
  test_assert(kmxplus->layr != nullptr);

  test_assert(kmxplus->key2Helper.valid());
  test_assert(kmxplus->layrHelper.valid());

  // First, find the string as an id
  // TODO-LDML: will not work for multi string cases
  KMX_DWORD strId = kmxplus->strs->find(chstr); // not an error if chstr is 0, may be single ch

  // OK. Now we can search the keybag
  KMX_DWORD keyIndex = 0;
  auto *key2 = kmxplus->key2Helper.findKeyByStringTo(chstr, strId, keyIndex);
  if (key2 == nullptr) {
    fillin.formatType(__FILE__, __LINE__, LDML_ACTION_FAIL, u"No key for repertoire test: ", chstr);
    return;
  }

  // Now, look for the _first_ candidate vkey match in the kmap.
  for (KMX_DWORD kmapIndex = 0; kmapIndex < kmxplus->key2->kmapCount; kmapIndex++) {
    auto *kmap = kmxplus->key2Helper.getKmap(kmapIndex);
    test_assert(kmap != nullptr);
    if (kmap->key == keyIndex) {
      fillin.k = {(km_core_virtual_key)kmap->vkey, (uint16_t)kmap->mod};
      std::cout << "found vkey " << fillin.k.vk << ":" << fillin.k.modifier_state << std::endl;
      fillin.type = LDML_ACTION_KEY_EVENT;
      return;
    }
  }

  fillin.formatType(__FILE__, __LINE__, LDML_ACTION_FAIL, u"Could not find candidate vkey: ", chstr);
}

const std::u16string &
LdmlJsonRepertoireTestSource::get_context() {
  return context; // no context needed
}

int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data, const km::core::path &compiled) {
  this->data = data;  // TODO-LDML
  // Need an update to json.hpp to use contains()
  // if (data.contains("/type"_json_pointer)) {
  if (data.find("type") != data.end()) {
    type = data["/type"_json_pointer].get<std::string>();
  } else {
    type = "default";
  }
  chars      = data["/chars"_json_pointer].get<std::string>();
  std::cout << "Loaded " << path << " = " << this->type << " || " << this->chars << std::endl;
  UErrorCode status = U_ZERO_ERROR;
  icu::StringPiece piece(chars);
  icu::UnicodeString pattern = icu::UnicodeString::fromUTF8(piece);
  // icu::UnicodeString pattern(chars.data(), (int32_t)chars.length());
  uset = std::unique_ptr<icu::UnicodeSet>(new icu::UnicodeSet(pattern, status));
  if (U_FAILURE(status)) {
    // could be a malformed syntax isue
    std::cerr << "UnicodeSet c'tor problem " << u_errorName(status) << std::endl;
    return 1;
  }
  std::cout << "Got UnicodeSet of " << uset->size() << " char(s)." << std::endl;
  // #if (U_ICU_VERSION_MAJOR_NUM >= 70)
  // // TODO-LDML: function was private previously
  // if (uset->hasStrings()) {
  //   // illegal unicodeset of this form:  [a b c {this_is_a_string}]
  //   std::cerr << "Spec err: may not have strings. " << chars << std::endl;
  //   return 1;
  // }
  // #endif
  iterator = std::unique_ptr<icu::UnicodeSetIterator>(new icu::UnicodeSetIterator(*uset));
  return load_kmx_plus(compiled);
}
#endif // HAVE_ICU4C

LdmlJsonTestSourceFactory::LdmlJsonTestSourceFactory() : test_map() {
}

km::core::path
LdmlJsonTestSourceFactory::kmx_to_test_json(const km::core::path &kmx) {
  km::core::path p = kmx;
  p.replace_extension(TEST_JSON_SUFFIX);
  return p;
}

int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::core::path &path) {
  std::ifstream json_file(path.native());
  if (!json_file) {
    return -1; // no file
  }
  nlohmann::json data = nlohmann::json::parse(json_file);
  if (data.empty()) {
    return __LINE__; // empty
  }


  auto conformsTo = data["/keyboardTest3/conformsTo"_json_pointer].get<std::string>();
  assert_or_return(std::string(LDML_CLDR_TEST_VERSION_LATEST) == conformsTo);
  auto info_keyboard = data["/keyboardTest3/info/keyboard"_json_pointer].get<std::string>();
  auto info_author = data["/keyboardTest3/info/author"_json_pointer].get<std::string>();
  auto info_name = data["/keyboardTest3/info/name"_json_pointer].get<std::string>();
  // TODO-LDML: store these elsewhere?
  std::wcout << console_color::fg(console_color::BLUE) << "test file     = " << path.name().c_str() << console_color::reset() << std::endl;
  std::wcout << console_color::fg(console_color::YELLOW) << info_name.c_str() << "/ " << console_color::reset()
             << " test: " << info_keyboard.c_str() << " author: " << info_author.c_str() << std::endl;

  auto all_tests = data["/keyboardTest3/tests"_json_pointer];
  assert_or_return((!all_tests.empty()) && (all_tests.size() > 0));  // TODO-LDML: can be empty if repertoire only?

  for(auto tests : all_tests) {
    auto tests_name = tests["/name"_json_pointer].get<std::string>();
    for (auto test : tests["/test"_json_pointer]) {
      auto test_name = test["/name"_json_pointer].get<std::string>();
      std::string test_path;
      test_path.append(info_name).append("/tests/").append(tests_name).append("/").append(test_name);
      // std::cout << "JSON: reading " << info_name << "/" << test_path << std::endl;

      std::unique_ptr<LdmlJsonTestSource> subtest(new LdmlJsonTestSource(test_path));
      assert_or_return(subtest->load(test, compiled) == 0);
      test_map[test_path] = std::unique_ptr<LdmlTestSource>(subtest.release());
    }
  }

#if defined(HAVE_ICU4C)
  auto rep_tests = data["/keyboardTest3/repertoire"_json_pointer];
  assert_or_return((!rep_tests.empty()) && (rep_tests.size() > 0));  // TODO-LDML: can be empty if tests only?

  for(auto rep : rep_tests) {
    auto rep_name  = rep["/name"_json_pointer].get<std::string>();

    std::string test_path;
    test_path.append(info_name).append("/repertoire/").append(rep_name);

    std::unique_ptr<LdmlJsonRepertoireTestSource> reptest(new LdmlJsonRepertoireTestSource(test_path));
    assert_or_return(reptest->load(rep, compiled) == 0);
    test_map[test_path] = std::unique_ptr<LdmlTestSource>(reptest.release());
  }


#else
  std::cerr << "Warning: HAVE_ICU4C not defined, so not enabling repertoire tests" << std::endl;
#endif

  return 0;
}

const JsonTestMap&
LdmlJsonTestSourceFactory::get_tests() const {
  return test_map;
}


}  // namespace tests
}  // namespace km