| 1 | import os |
| 2 | |
| 3 | const export_attr_vexe = @VEXE |
| 4 | const export_attr_tests_dir = os.dir(@FILE) |
| 5 | const export_attr_v3_dir = os.dir(export_attr_tests_dir) |
| 6 | const export_attr_vlib_dir = os.dir(export_attr_v3_dir) |
| 7 | const export_attr_v3_src = os.join_path(export_attr_v3_dir, 'v3.v') |
| 8 | |
| 9 | fn export_attr_build_v3() string { |
| 10 | v3_bin := os.join_path(os.temp_dir(), 'v3_export_attr_test_${os.getpid()}') |
| 11 | os.rm(v3_bin) or {} |
| 12 | build := |
| 13 | os.execute('${export_attr_vexe} -gc none -path "${export_attr_vlib_dir}|@vlib|@vmodules" -o ${v3_bin} ${export_attr_v3_src}') |
| 14 | assert build.exit_code == 0, build.output |
| 15 | return v3_bin |
| 16 | } |
| 17 | |
| 18 | fn export_attr_project(name string, files map[string]string) string { |
| 19 | root := os.join_path(os.temp_dir(), 'v3_export_attr_${name}_${os.getpid()}') |
| 20 | os.rmdir_all(root) or {} |
| 21 | mut rels := files.keys() |
| 22 | rels.sort() |
| 23 | for rel in rels { |
| 24 | path := os.join_path(root, rel) |
| 25 | os.mkdir_all(os.dir(path)) or { panic(err) } |
| 26 | os.write_file(path, files[rel]) or { panic(err) } |
| 27 | } |
| 28 | return root |
| 29 | } |
| 30 | |
| 31 | fn export_attr_compile(v3_bin string, main_file string, output string) os.Result { |
| 32 | return os.execute('${v3_bin} ${main_file} -b c -o ${output}') |
| 33 | } |
| 34 | |
| 35 | fn test_exported_imported_function_is_rooted_and_emitted_as_raw_symbol() { |
| 36 | v3_bin := export_attr_build_v3() |
| 37 | root := export_attr_project('imported_link', { |
| 38 | 'main.v': 'module main |
| 39 | |
| 40 | import expmod |
| 41 | |
| 42 | fn C.raw_exported_answer() int |
| 43 | |
| 44 | fn take_callback(cb fn () int) int { |
| 45 | return cb() |
| 46 | } |
| 47 | |
| 48 | fn main() { |
| 49 | println(C.raw_exported_answer().str()) |
| 50 | println(expmod.exported_answer().str()) |
| 51 | println(take_callback(expmod.exported_answer).str()) |
| 52 | } |
| 53 | ' |
| 54 | 'expmod/mod.v': "module expmod |
| 55 | |
| 56 | @[export: 'raw_exported_answer'] |
| 57 | pub fn exported_answer() int { |
| 58 | return helper_used() |
| 59 | } |
| 60 | |
| 61 | fn helper_used() int { |
| 62 | return 41 |
| 63 | } |
| 64 | |
| 65 | fn helper_unused() int { |
| 66 | return 99 |
| 67 | } |
| 68 | " |
| 69 | }) |
| 70 | bin_path := os.join_path(root, 'app') |
| 71 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), bin_path) |
| 72 | assert compile.exit_code == 0, compile.output |
| 73 | run := os.execute(bin_path) |
| 74 | assert run.exit_code == 0, run.output |
| 75 | assert run.output.trim_space() == '41\n41\n41', run.output |
| 76 | |
| 77 | c_code := os.read_file(bin_path + '.c') or { panic(err) } |
| 78 | assert c_code.contains('int expmod__exported_answer(void) {'), c_code |
| 79 | assert c_code.contains('int raw_exported_answer(void) {'), c_code |
| 80 | assert c_code.contains('return expmod__exported_answer();'), c_code |
| 81 | assert c_code.contains('raw_exported_answer()'), c_code |
| 82 | assert c_code.contains('take_callback(expmod__exported_answer)'), c_code |
| 83 | assert c_code.contains('expmod__helper_used('), c_code |
| 84 | assert !c_code.contains('expmod__helper_unused('), c_code |
| 85 | } |
| 86 | |
| 87 | fn test_duplicate_export_name_is_rejected() { |
| 88 | v3_bin := export_attr_build_v3() |
| 89 | root := export_attr_project('duplicate', { |
| 90 | 'main.v': "module main |
| 91 | |
| 92 | @[export: 'raw_duplicate'] |
| 93 | fn one() {} |
| 94 | |
| 95 | @[export: 'raw_duplicate'] |
| 96 | fn two() {} |
| 97 | |
| 98 | fn main() {} |
| 99 | " |
| 100 | }) |
| 101 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 102 | assert compile.exit_code != 0, compile.output |
| 103 | assert compile.output.contains('duplicate export name `raw_duplicate`'), compile.output |
| 104 | } |
| 105 | |
| 106 | fn test_disabled_export_attr_does_not_register_raw_symbol() { |
| 107 | v3_bin := export_attr_build_v3() |
| 108 | root := export_attr_project('disabled_export', { |
| 109 | 'main.v': "module main |
| 110 | |
| 111 | @[if missing_export_flag ?] |
| 112 | @[export: 'raw_disabled_export'] |
| 113 | fn disabled_export() int { |
| 114 | return 2 |
| 115 | } |
| 116 | |
| 117 | @[export: 'raw_enabled_export'] |
| 118 | fn enabled_export() int { |
| 119 | return 7 |
| 120 | } |
| 121 | |
| 122 | fn main() { |
| 123 | println(enabled_export().str()) |
| 124 | } |
| 125 | " |
| 126 | }) |
| 127 | bin_path := os.join_path(root, 'app') |
| 128 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), bin_path) |
| 129 | assert compile.exit_code == 0, compile.output |
| 130 | run := os.execute(bin_path) |
| 131 | assert run.exit_code == 0, run.output |
| 132 | assert run.output.trim_space() == '7', run.output |
| 133 | |
| 134 | c_code := os.read_file(bin_path + '.c') or { panic(err) } |
| 135 | assert c_code.contains('int raw_enabled_export(void) {'), c_code |
| 136 | assert !c_code.contains('raw_disabled_export'), c_code |
| 137 | } |
| 138 | |
| 139 | fn test_export_name_collision_with_runtime_symbol_is_rejected() { |
| 140 | v3_bin := export_attr_build_v3() |
| 141 | root := export_attr_project('runtime_collision', { |
| 142 | 'main.v': "module main |
| 143 | |
| 144 | @[export: 'v_free'] |
| 145 | fn collides_with_runtime_free() {} |
| 146 | |
| 147 | fn main() {} |
| 148 | " |
| 149 | }) |
| 150 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 151 | assert compile.exit_code != 0, compile.output |
| 152 | assert compile.output.contains('export name `v_free`'), compile.output |
| 153 | } |
| 154 | |
| 155 | fn test_export_name_collision_with_natural_symbol_is_rejected() { |
| 156 | v3_bin := export_attr_build_v3() |
| 157 | root := export_attr_project('natural_collision', { |
| 158 | 'main.v': "module main |
| 159 | |
| 160 | @[export: 'natural_name'] |
| 161 | fn natural_name() int { |
| 162 | return 1 |
| 163 | } |
| 164 | |
| 165 | fn main() {} |
| 166 | " |
| 167 | }) |
| 168 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 169 | assert compile.exit_code != 0, compile.output |
| 170 | assert compile.output.contains('export name `natural_name`'), compile.output |
| 171 | } |
| 172 | |
| 173 | fn test_export_name_collision_with_libc_remapped_natural_symbol_is_rejected() { |
| 174 | v3_bin := export_attr_build_v3() |
| 175 | root := export_attr_project('libc_remapped_natural_collision', { |
| 176 | 'main.v': "module main |
| 177 | |
| 178 | fn rint() int { |
| 179 | return 1 |
| 180 | } |
| 181 | |
| 182 | @[export: 'v_rint'] |
| 183 | fn exported_rint_collision() int { |
| 184 | return 2 |
| 185 | } |
| 186 | |
| 187 | fn main() {} |
| 188 | " |
| 189 | }) |
| 190 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 191 | assert compile.exit_code != 0, compile.output |
| 192 | assert compile.output.contains('export name `v_rint`'), compile.output |
| 193 | assert compile.output.contains('collides with `rint`'), compile.output |
| 194 | assert !compile.output.contains('C compilation failed'), compile.output |
| 195 | assert !compile.output.contains('redefinition'), compile.output |
| 196 | } |
| 197 | |
| 198 | fn test_export_main_collision_with_top_level_script_entry_is_rejected() { |
| 199 | v3_bin := export_attr_build_v3() |
| 200 | root := export_attr_project('top_level_script_export_main_collision', { |
| 201 | 'main.v': " |
| 202 | @[export: 'main'] |
| 203 | fn exported_entry() {} |
| 204 | |
| 205 | println('script') |
| 206 | " |
| 207 | }) |
| 208 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 209 | assert compile.exit_code != 0, compile.output |
| 210 | assert compile.output.contains('export name `main` for `exported_entry`'), compile.output |
| 211 | assert compile.output.contains('synthetic entry point `main`'), compile.output |
| 212 | assert !compile.output.contains('C compilation failed'), compile.output |
| 213 | assert !compile.output.contains('redefinition'), compile.output |
| 214 | } |
| 215 | |
| 216 | fn test_export_main_collision_with_top_level_match_entry_is_rejected() { |
| 217 | v3_bin := export_attr_build_v3() |
| 218 | root := export_attr_project('top_level_match_export_main_collision', { |
| 219 | 'main.v': " |
| 220 | @[export: 'main'] |
| 221 | fn exported_entry() {} |
| 222 | |
| 223 | match 1 { |
| 224 | 1 { println('one') } |
| 225 | else { println('other') } |
| 226 | } |
| 227 | " |
| 228 | }) |
| 229 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 230 | assert compile.exit_code != 0, compile.output |
| 231 | assert compile.output.contains('export name `main` for `exported_entry`'), compile.output |
| 232 | assert compile.output.contains('synthetic entry point `main`'), compile.output |
| 233 | assert !compile.output.contains('C compilation failed'), compile.output |
| 234 | assert !compile.output.contains('redefinition'), compile.output |
| 235 | } |
| 236 | |
| 237 | fn test_export_main_collision_with_test_harness_entry_is_rejected() { |
| 238 | v3_bin := export_attr_build_v3() |
| 239 | root := export_attr_project('test_harness_export_main_collision', { |
| 240 | 'main_test.v': " |
| 241 | @[export: 'main'] |
| 242 | fn exported_entry() {} |
| 243 | |
| 244 | fn test_ok() {} |
| 245 | " |
| 246 | }) |
| 247 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main_test.v'), os.join_path(root, |
| 248 | 'app')) |
| 249 | assert compile.exit_code != 0, compile.output |
| 250 | assert compile.output.contains('export name `main` for `exported_entry`'), compile.output |
| 251 | assert compile.output.contains('synthetic entry point `main`'), compile.output |
| 252 | assert !compile.output.contains('C compilation failed'), compile.output |
| 253 | assert !compile.output.contains('redefinition'), compile.output |
| 254 | } |
| 255 | |
| 256 | fn test_export_main_in_non_main_test_module_does_not_report_synthetic_collision() { |
| 257 | v3_bin := export_attr_build_v3() |
| 258 | root := export_attr_project('test_harness_non_main_module_export_main', { |
| 259 | 'foo_test.v': "module foo |
| 260 | |
| 261 | @[export: 'main'] |
| 262 | fn exported_entry() {} |
| 263 | |
| 264 | fn test_ok() {} |
| 265 | " |
| 266 | }) |
| 267 | compile := export_attr_compile(v3_bin, os.join_path(root, 'foo_test.v'), os.join_path(root, |
| 268 | 'app')) |
| 269 | assert compile.exit_code != 0, compile.output |
| 270 | assert compile.output.contains('only module main test files are supported'), compile.output |
| 271 | assert !compile.output.contains('synthetic entry point `main`'), compile.output |
| 272 | assert !compile.output.contains('C compilation failed'), compile.output |
| 273 | assert !compile.output.contains('redefinition'), compile.output |
| 274 | } |
| 275 | |
| 276 | fn test_invalid_export_names_are_rejected() { |
| 277 | v3_bin := export_attr_build_v3() |
| 278 | root := export_attr_project('invalid_names', { |
| 279 | 'main.v': "module main |
| 280 | |
| 281 | @[export: '1bad'] |
| 282 | fn bad_digit() {} |
| 283 | |
| 284 | @[export: 'for'] |
| 285 | fn bad_keyword() {} |
| 286 | |
| 287 | fn main() {} |
| 288 | " |
| 289 | }) |
| 290 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 291 | assert compile.exit_code != 0, compile.output |
| 292 | assert compile.output.contains('invalid export name `1bad`'), compile.output |
| 293 | assert compile.output.contains('invalid export name `for`'), compile.output |
| 294 | } |
| 295 | |
| 296 | fn test_invalid_imported_export_name_is_rejected_before_cgen() { |
| 297 | v3_bin := export_attr_build_v3() |
| 298 | root := export_attr_project('invalid_imported_name', { |
| 299 | 'main.v': 'module main |
| 300 | |
| 301 | import badexp |
| 302 | |
| 303 | fn main() { |
| 304 | println(badexp.answer().str()) |
| 305 | } |
| 306 | ' |
| 307 | 'badexp/badexp.v': "module badexp |
| 308 | |
| 309 | @[export: '1bad'] |
| 310 | pub fn answer() int { |
| 311 | return 1 |
| 312 | } |
| 313 | " |
| 314 | }) |
| 315 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 316 | assert compile.exit_code != 0 |
| 317 | assert compile.output.contains('invalid export name `1bad` for `badexp.answer`'), compile.output |
| 318 | } |
| 319 | |
| 320 | fn test_export_name_reserved_by_v3_c_preamble_is_rejected() { |
| 321 | v3_bin := export_attr_build_v3() |
| 322 | root := export_attr_project('preamble_reserved_name', { |
| 323 | 'main.v': "module main |
| 324 | |
| 325 | @[export: 'bool'] |
| 326 | fn collides_with_preamble_bool() bool { |
| 327 | return true |
| 328 | } |
| 329 | |
| 330 | @[export: 'string'] |
| 331 | fn collides_with_preamble_string() {} |
| 332 | |
| 333 | @[export: 'voidptr'] |
| 334 | fn collides_with_preamble_voidptr() {} |
| 335 | |
| 336 | @[export: 'i8'] |
| 337 | fn collides_with_preamble_i8() {} |
| 338 | |
| 339 | @[export: 'true'] |
| 340 | fn collides_with_preamble_true() {} |
| 341 | |
| 342 | @[export: 'Array'] |
| 343 | fn collides_with_runtime_array() {} |
| 344 | |
| 345 | @[export: 'map'] |
| 346 | fn collides_with_runtime_map() int { |
| 347 | values := map[string]int{ |
| 348 | 'a': 1 |
| 349 | } |
| 350 | return values['a'] or { 0 } |
| 351 | } |
| 352 | |
| 353 | @[export: 'DenseArray'] |
| 354 | fn collides_with_runtime_dense_array() {} |
| 355 | |
| 356 | @[export: 'SortedMap'] |
| 357 | fn collides_with_runtime_sorted_map() {} |
| 358 | |
| 359 | @[export: 'Optional'] |
| 360 | fn collides_with_runtime_optional() {} |
| 361 | |
| 362 | fn main() {} |
| 363 | " |
| 364 | }) |
| 365 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 366 | assert compile.exit_code != 0, compile.output |
| 367 | assert compile.output.contains('invalid export name `bool`'), compile.output |
| 368 | assert compile.output.contains('invalid export name `string`'), compile.output |
| 369 | assert compile.output.contains('invalid export name `voidptr`'), compile.output |
| 370 | assert compile.output.contains('invalid export name `i8`'), compile.output |
| 371 | assert compile.output.contains('invalid export name `true`'), compile.output |
| 372 | assert compile.output.contains('invalid export name `Array`'), compile.output |
| 373 | assert compile.output.contains('invalid export name `map`'), compile.output |
| 374 | assert compile.output.contains('invalid export name `DenseArray`'), compile.output |
| 375 | assert compile.output.contains('invalid export name `SortedMap`'), compile.output |
| 376 | assert compile.output.contains('invalid export name `Optional`'), compile.output |
| 377 | } |
| 378 | |
| 379 | fn test_generic_export_is_rejected_fail_closed() { |
| 380 | v3_bin := export_attr_build_v3() |
| 381 | root := export_attr_project('generic_export', { |
| 382 | 'main.v': "module main |
| 383 | |
| 384 | @[export: 'raw_generic'] |
| 385 | fn raw_generic[T](value T) T { |
| 386 | return value |
| 387 | } |
| 388 | |
| 389 | fn main() {} |
| 390 | " |
| 391 | }) |
| 392 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 393 | assert compile.exit_code != 0, compile.output |
| 394 | assert compile.output.contains('generic function `raw_generic` cannot be exported'), compile.output |
| 395 | } |
| 396 | |
| 397 | fn test_exported_function_must_name_params() { |
| 398 | v3_bin := export_attr_build_v3() |
| 399 | root := export_attr_project('unnamed_param', { |
| 400 | 'main.v': "module main |
| 401 | |
| 402 | @[export: 'raw_unnamed'] |
| 403 | fn unnamed(int) int { |
| 404 | return 0 |
| 405 | } |
| 406 | |
| 407 | fn main() {} |
| 408 | " |
| 409 | }) |
| 410 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), os.join_path(root, 'app')) |
| 411 | assert compile.exit_code != 0, compile.output |
| 412 | assert compile.output.contains('must name all parameters'), compile.output |
| 413 | } |
| 414 | |
| 415 | fn test_export_wrapper_fixed_array_return_uses_return_wrapper_type() { |
| 416 | v3_bin := export_attr_build_v3() |
| 417 | root := export_attr_project('fixed_array_return', { |
| 418 | 'main.v': "module main |
| 419 | |
| 420 | @[export: 'raw_numbers'] |
| 421 | fn numbers() [3]int { |
| 422 | mut values := [3]int{} |
| 423 | values[0] = 1 |
| 424 | values[1] = 2 |
| 425 | values[2] = 3 |
| 426 | return values |
| 427 | } |
| 428 | |
| 429 | fn main() {} |
| 430 | " |
| 431 | }) |
| 432 | bin_path := os.join_path(root, 'app') |
| 433 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), bin_path) |
| 434 | assert compile.exit_code == 0, compile.output |
| 435 | c_code := os.read_file(bin_path + '.c') or { panic(err) } |
| 436 | assert c_code.contains('_v_ret_Array_fixed_int_3 numbers(void);'), c_code |
| 437 | assert c_code.contains('_v_ret_Array_fixed_int_3 raw_numbers(void);'), c_code |
| 438 | assert c_code.contains('_v_ret_Array_fixed_int_3 numbers(void) {'), c_code |
| 439 | assert c_code.contains('_v_ret_Array_fixed_int_3 raw_numbers(void) {'), c_code |
| 440 | assert c_code.contains('return numbers();'), c_code |
| 441 | assert !c_code.contains('\nArray_fixed_int_3 raw_numbers(void)'), c_code |
| 442 | } |
| 443 | |
| 444 | fn test_export_wrapper_fn_pointer_return_uses_fn_pointer_typedef() { |
| 445 | v3_bin := export_attr_build_v3() |
| 446 | root := export_attr_project('fn_pointer_return', { |
| 447 | 'main.v': "module main |
| 448 | |
| 449 | type Callback = fn () int |
| 450 | |
| 451 | fn callback_value() int { |
| 452 | return 7 |
| 453 | } |
| 454 | |
| 455 | @[export: 'raw_callback'] |
| 456 | fn callback() Callback { |
| 457 | return callback_value |
| 458 | } |
| 459 | |
| 460 | fn main() {} |
| 461 | " |
| 462 | }) |
| 463 | bin_path := os.join_path(root, 'app') |
| 464 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), bin_path) |
| 465 | assert compile.exit_code == 0, compile.output |
| 466 | c_code := os.read_file(bin_path + '.c') or { panic(err) } |
| 467 | assert c_code.contains('typedef int (*_fn_ptr_'), c_code |
| 468 | assert c_code.contains(' callback(void);'), c_code |
| 469 | assert c_code.contains(' raw_callback(void);'), c_code |
| 470 | assert c_code.contains(' raw_callback(void) {'), c_code |
| 471 | assert c_code.contains('return callback();'), c_code |
| 472 | assert !c_code.contains('fn_ptr:'), c_code |
| 473 | } |
| 474 | |
| 475 | fn test_export_wrapper_for_veb_handler_forwards_implicit_ctx() { |
| 476 | v3_bin := export_attr_build_v3() |
| 477 | root := export_attr_project('veb_implicit_ctx_export_wrapper', { |
| 478 | 'main.v': "module main |
| 479 | |
| 480 | import veb |
| 481 | |
| 482 | pub struct Context { |
| 483 | veb.Context |
| 484 | } |
| 485 | |
| 486 | pub struct App {} |
| 487 | |
| 488 | @[export: 'raw_show'] |
| 489 | pub fn (app &App) show(id int) veb.Result { |
| 490 | _ = id |
| 491 | return veb.Result{} |
| 492 | } |
| 493 | |
| 494 | fn main() {} |
| 495 | " |
| 496 | }) |
| 497 | c_path := os.join_path(root, 'app.c') |
| 498 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), c_path) |
| 499 | assert compile.exit_code == 0, compile.output |
| 500 | c_code := os.read_file(c_path) or { panic(err) } |
| 501 | assert c_code.contains('veb__Result raw_show(App* app, Context* ctx, int id);'), c_code |
| 502 | assert c_code.contains('veb__Result raw_show(App* app, Context* ctx, int id) {'), c_code |
| 503 | assert c_code.contains('return App__show(app, ctx, id);'), c_code |
| 504 | } |
| 505 | |
| 506 | fn test_export_wrapper_for_veb_handler_underscore_param_matches_implicit_ctx_position() { |
| 507 | v3_bin := export_attr_build_v3() |
| 508 | root := export_attr_project('veb_implicit_ctx_underscore_export_wrapper', { |
| 509 | 'main.v': "module main |
| 510 | |
| 511 | import veb |
| 512 | |
| 513 | pub struct Context { |
| 514 | veb.Context |
| 515 | } |
| 516 | |
| 517 | pub struct App {} |
| 518 | |
| 519 | @[export: 'raw_show_underscore'] |
| 520 | pub fn (app &App) show(_ int) veb.Result { |
| 521 | return veb.Result{} |
| 522 | } |
| 523 | |
| 524 | fn main() {} |
| 525 | " |
| 526 | }) |
| 527 | c_path := os.join_path(root, 'app.c') |
| 528 | compile := export_attr_compile(v3_bin, os.join_path(root, 'main.v'), c_path) |
| 529 | assert compile.exit_code == 0, compile.output |
| 530 | c_code := os.read_file(c_path) or { panic(err) } |
| 531 | assert c_code.contains('veb__Result raw_show_underscore(App* app, Context* ctx, int _2);'), c_code |
| 532 | assert c_code.contains('veb__Result raw_show_underscore(App* app, Context* ctx, int _2) {'), c_code |
| 533 | assert c_code.contains('return App__show(app, ctx, _2);'), c_code |
| 534 | assert !c_code.contains('return App__show(app, ctx, _1);'), c_code |
| 535 | } |
| 536 | |