@@ -5,6 +5,8 @@ use qmk2zmk::zmk;
55const KEYMAP_C : & str =
66 include_str ! ( "../examples/zsa-qmk/zsa_planck_ez_glow_planck_source/keymap.c" ) ;
77
8+ const ZMK_KEYMAP : & str = include_str ! ( "../examples/zmk/keymap.zmk" ) ;
9+
810#[ test]
911fn parses_four_layers ( ) {
1012 let km = qmk2zmk:: qmk:: parse_c:: parse ( KEYMAP_C ) . unwrap ( ) ;
@@ -138,3 +140,209 @@ fn output_is_valid_zmk_skeleton() {
138140 // Macro stub present
139141 assert ! ( out. contains( "ST_MACRO_0: ST_MACRO_0" ) ) ;
140142}
143+
144+ // ── ZMK example: parsing ──────────────────────────────────────────────────────
145+
146+ #[ test]
147+ fn zmk_parses_four_layers ( ) {
148+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
149+ assert_eq ! ( km. layers. len( ) , 4 ) ;
150+ }
151+
152+ #[ test]
153+ fn zmk_layers_named_correctly ( ) {
154+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
155+ assert_eq ! ( km. layers[ 0 ] . name, "base_layer" ) ;
156+ assert_eq ! ( km. layers[ 1 ] . name, "lower_layer" ) ;
157+ assert_eq ! ( km. layers[ 2 ] . name, "raise_layer" ) ;
158+ assert_eq ! ( km. layers[ 3 ] . name, "adjust_layer" ) ;
159+ }
160+
161+ #[ test]
162+ fn zmk_has_48_keys_per_layer ( ) {
163+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
164+ for layer in & km. layers {
165+ assert_eq ! ( layer. keys. len( ) , 48 , "layer {} has wrong key count" , layer. name) ;
166+ }
167+ }
168+
169+ #[ test]
170+ fn zmk_base_layer_first_key_is_tab ( ) {
171+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
172+ let base = & km. layers [ 0 ] ;
173+ assert ! ( matches!( & base. keys[ 0 ] , Key :: Kp ( k) if k == "TAB" ) ) ;
174+ }
175+
176+ #[ test]
177+ fn zmk_base_layer_has_caps_word ( ) {
178+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
179+ let base = & km. layers [ 0 ] ;
180+ // Row 1, col 0
181+ assert ! ( matches!( & base. keys[ 12 ] , Key :: CapsWord ) ) ;
182+ }
183+
184+ #[ test]
185+ fn zmk_base_layer_has_home_row_mods ( ) {
186+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
187+ let base = & km. layers [ 0 ] ;
188+ // Row 2: mt LALT Z, mt LCTRL X
189+ assert ! ( matches!( & base. keys[ 25 ] , Key :: Mt ( m, k) if m == "LALT" && k == "Z" ) ) ;
190+ assert ! ( matches!( & base. keys[ 26 ] , Key :: Mt ( m, k) if m == "LCTRL" && k == "X" ) ) ;
191+ }
192+
193+ #[ test]
194+ fn zmk_base_layer_has_mo_keys ( ) {
195+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
196+ let base = & km. layers [ 0 ] ;
197+ assert ! ( matches!( & base. keys[ 40 ] , Key :: Mo ( 1 ) ) , "col 4 of bottom row should be mo 1" ) ;
198+ assert ! ( matches!( & base. keys[ 43 ] , Key :: Mo ( 2 ) ) , "col 7 of bottom row should be mo 2" ) ;
199+ }
200+
201+ #[ test]
202+ fn zmk_lower_layer_has_symbols ( ) {
203+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
204+ let lower = & km. layers [ 1 ] ;
205+ assert ! ( matches!( & lower. keys[ 0 ] , Key :: Kp ( k) if k == "TILDE" ) ) ;
206+ assert ! ( matches!( & lower. keys[ 1 ] , Key :: Kp ( k) if k == "EXCL" ) ) ;
207+ assert ! ( matches!( & lower. keys[ 2 ] , Key :: Kp ( k) if k == "AT" ) ) ;
208+ assert ! ( matches!( & lower. keys[ 3 ] , Key :: Kp ( k) if k == "HASH" ) ) ;
209+ }
210+
211+ #[ test]
212+ fn zmk_lower_layer_has_trans_and_nav ( ) {
213+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
214+ let lower = & km. layers [ 1 ] ;
215+ assert ! ( matches!( & lower. keys[ 12 ] , Key :: Trans ) ) ;
216+ assert ! ( matches!( & lower. keys[ 44 ] , Key :: Kp ( k) if k == "HOME" ) ) ;
217+ assert ! ( matches!( & lower. keys[ 47 ] , Key :: Kp ( k) if k == "END" ) ) ;
218+ }
219+
220+ #[ test]
221+ fn zmk_raise_layer_has_numbers ( ) {
222+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
223+ let raise = & km. layers [ 2 ] ;
224+ assert ! ( matches!( & raise. keys[ 1 ] , Key :: Kp ( k) if k == "N1" ) ) ;
225+ assert ! ( matches!( & raise. keys[ 9 ] , Key :: Kp ( k) if k == "N9" ) ) ;
226+ assert ! ( matches!( & raise. keys[ 10 ] , Key :: Kp ( k) if k == "N0" ) ) ;
227+ }
228+
229+ #[ test]
230+ fn zmk_adjust_layer_has_bootloader_and_reset ( ) {
231+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
232+ let adjust = & km. layers [ 3 ] ;
233+ assert ! ( matches!( & adjust. keys[ 0 ] , Key :: Bootloader ) ) ;
234+ assert ! ( matches!( & adjust. keys[ 11 ] , Key :: SysReset ) ) ;
235+ }
236+
237+ #[ test]
238+ fn zmk_adjust_layer_has_rgb ( ) {
239+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
240+ let adjust = & km. layers [ 3 ] ;
241+ assert ! ( matches!( & adjust. keys[ 44 ] , Key :: RgbUg ( a) if a == "RGB_TOG" ) ) ;
242+ assert ! ( matches!( & adjust. keys[ 47 ] , Key :: RgbUg ( a) if a == "RGB_EFF" ) ) ;
243+ }
244+
245+ #[ test]
246+ fn zmk_tri_layer_detected ( ) {
247+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
248+ let tri = km. tri_layer . expect ( "tri-layer should be detected" ) ;
249+ assert_eq ! ( tri. lower, 1 ) ;
250+ assert_eq ! ( tri. upper, 2 ) ;
251+ assert_eq ! ( tri. tri, 3 ) ;
252+ }
253+
254+ // ── ZMK example: rendering to QMK ────────────────────────────────────────────
255+
256+ #[ test]
257+ fn zmk_renders_to_valid_qmk_c ( ) {
258+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
259+ let out = qmk2zmk:: qmk:: render_c ( & km, None ) ;
260+ assert ! ( out. contains( "#include QMK_KEYBOARD_H" ) ) ;
261+ assert ! ( out. contains( "keymaps[][MATRIX_ROWS][MATRIX_COLS]" ) ) ;
262+ assert ! ( out. contains( "_BASE" ) ) ;
263+ assert ! ( out. contains( "_LOWER" ) ) ;
264+ assert ! ( out. contains( "_RAISE" ) ) ;
265+ assert ! ( out. contains( "_ADJUST" ) ) ;
266+ assert ! ( out. contains( "KC_TAB" ) ) ;
267+ assert ! ( out. contains( "CW_TOGG" ) ) ;
268+ assert ! ( out. contains( "MO(1)" ) ) ;
269+ assert ! ( out. contains( "MO(2)" ) ) ;
270+ }
271+
272+ #[ test]
273+ fn zmk_home_row_mods_survive_to_qmk ( ) {
274+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
275+ let out = qmk2zmk:: qmk:: render_c ( & km, None ) ;
276+ assert ! ( out. contains( "MT(MOD_LALT,KC_Z)" ) ) ;
277+ assert ! ( out. contains( "MT(MOD_LCTL,KC_X)" ) ) ;
278+ }
279+
280+ #[ test]
281+ fn zmk_rgb_survives_to_qmk ( ) {
282+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
283+ let out = qmk2zmk:: qmk:: render_c ( & km, None ) ;
284+ assert ! ( out. contains( "RGB_TOG" ) ) ;
285+ }
286+
287+ // ── Round-trip tests ──────────────────────────────────────────────────────────
288+
289+ /// ZMK → render ZMK → parse → render ZMK again: the two renders must be equal.
290+ /// This verifies our ZMK renderer is stable (idempotent after the first parse).
291+ #[ test]
292+ fn zmk_to_zmk_round_trip ( ) {
293+ let km = zmk:: parse:: parse ( ZMK_KEYMAP ) . unwrap ( ) ;
294+ let pass1 = zmk:: render ( & km, None ) ;
295+ let km2 = zmk:: parse:: parse ( & pass1) . unwrap ( ) ;
296+ let pass2 = zmk:: render ( & km2, None ) ;
297+ assert_eq ! ( pass1, pass2, "ZMK render must be stable after re-parse" ) ;
298+ }
299+
300+ /// QMK C (no unknown keys) → render ZMK → re-parse → render again: the two ZMK
301+ /// renders must be equal. Uses an inline fixture so there are no Unknown keys
302+ /// that would become ZMK block comments and disappear on re-parse.
303+ #[ test]
304+ fn qmk_c_to_zmk_to_zmk_round_trip ( ) {
305+ const SIMPLE_QMK_C : & str = r"
306+ enum layers { _BASE, _FN };
307+ const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
308+ [_BASE] = LAYOUT(
309+ KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T,
310+ CW_TOGG, KC_A, KC_S, KC_D, KC_F, KC_G,
311+ KC_LSFT, MT(MOD_LALT,KC_Z), MT(MOD_LCTL,KC_X), KC_C, KC_V, KC_B,
312+ KC_LCTL, KC_LGUI, KC_LALT, KC_LCTL, MO(1), KC_SPC
313+ ),
314+ [_FN] = LAYOUT(
315+ KC_TRNS, KC_1, KC_2, KC_3, KC_4, KC_5,
316+ KC_TRNS, KC_NO, KC_NO, KC_NO, KC_NO, KC_NO,
317+ KC_TRNS, KC_NO, KC_NO, KC_NO, KC_NO, KC_NO,
318+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS
319+ ),
320+ };
321+ " ;
322+ let km = qmk_c:: parse ( SIMPLE_QMK_C ) . unwrap ( ) ;
323+ let pass1 = zmk:: render ( & km, None ) ;
324+ let km2 = zmk:: parse:: parse ( & pass1) . unwrap ( ) ;
325+ let pass2 = zmk:: render ( & km2, None ) ;
326+ assert_eq ! ( pass1, pass2, "ZMK render from QMK source must stabilize after one re-parse" ) ;
327+ }
328+
329+ /// QMK C → ZMK → parse: key bindings must be preserved for every layer that
330+ /// contains no Unknown keys. Unknown keys render as ZMK block comments which
331+ /// the re-parser strips, so layers with unknowns are explicitly skipped here.
332+ #[ test]
333+ fn qmk_c_to_zmk_preserves_all_keys ( ) {
334+ let km1 = qmk_c:: parse ( KEYMAP_C ) . unwrap ( ) ;
335+ let zmk_str = zmk:: render ( & km1, None ) ;
336+ let km2 = zmk:: parse:: parse ( & zmk_str) . unwrap ( ) ;
337+
338+ assert_eq ! ( km1. layers. len( ) , km2. layers. len( ) , "layer count must survive QMK→ZMK" ) ;
339+ for ( l1, l2) in km1. layers . iter ( ) . zip ( km2. layers . iter ( ) ) {
340+ if l1. keys . iter ( ) . any ( |k| matches ! ( k, Key :: Unknown ( _) ) ) {
341+ continue ; // unknown keys become block comments that vanish on re-parse
342+ }
343+ assert_eq ! (
344+ l1. keys, l2. keys,
345+ "layer {} keys must survive QMK→ZMK round-trip" , l1. name
346+ ) ;
347+ }
348+ }
0 commit comments