[{"data":1,"prerenderedAt":6034},["ShallowReactive",2],{"page-\u002Flua\u002F14-testing":3},{"id":4,"title":5,"body":6,"description":6025,"extension":6026,"meta":6027,"navigation":99,"path":6028,"seo":6029,"stem":6032,"__hash__":6033},"content\u002Flua\u002F14-testing.md","Lua 14 — Testing, Mocking & TDD",{"type":7,"value":8,"toc":6014},"minimark",[9,14,50,55,1104,1108,5607,5611,5617,5627,5633,5639,5643,5659,5669,5742,5755,5759,6010],[10,11,13],"h1",{"id":12},"_14-testing-mocking-tdd","14 — Testing, Mocking & TDD",[15,16,17,18,22,23,26,27,30,31,35,36,39,40,45,46,49],"p",{},"Lua has no built-in test framework, but the ecosystem provides ",[19,20,21],"code",{},"luaunit"," (lightweight), ",[19,24,25],{},"busted"," (BDD-style), and ",[19,28,29],{},"lustre"," (minimal). The key to testable Lua: ",[32,33,34],"strong",{},"dependency injection"," over hard ",[19,37,38],{},"require()",", ",[32,41,42],{},[19,43,44],{},"package.preload"," for mocking modules, and ",[32,47,48],{},"table-based tests"," for parametric coverage. This chapter covers unit testing, mocking, integration testing, and coverage.",[51,52,54],"h2",{"id":53},"test-framework-minimal-assertion-library","Test Framework: Minimal Assertion Library",[56,57,60],"code-wrapper",{"language":58,"filename":59},"lua","test\u002Fassert.lua",[61,62,66],"pre",{"className":63,"code":64,"language":58,"meta":65,"style":65},"language-lua shiki shiki-themes github-light github-dark","-- A minimal assertion library (no external deps) — 50 lines\nlocal Assert = {}\n\nfunction Assert.equals(expected, actual, msg)\n  if expected ~= actual then\n    error(string.format(\"%s: expected %s, got %s\",\n      msg or \"assertion failed\", tostring(expected), tostring(actual)), 2)\n  end\nend\n\nfunction Assert.not_equals(unexpected, actual, msg)\n  if unexpected == actual then\n    error(string.format(\"%s: expected NOT %s, got %s\",\n      msg or \"assertion failed\", tostring(unexpected), tostring(actual)), 2)\n  end\nend\n\nfunction Assert.is_nil(actual, msg)\n  if actual ~= nil then\n    error(string.format(\"%s: expected nil, got %s\",\n      msg or \"assertion failed\", tostring(actual)), 2)\n  end\nend\n\nfunction Assert.is_not_nil(actual, msg)\n  if actual == nil then\n    error(msg or \"expected non-nil, got nil\", 2)\n  end\nend\n\nfunction Assert.is_true(actual, msg)\n  if actual ~= true then  -- STRICT: only `true`, not truthy\n    error(string.format(\"%s: expected true, got %s (%s)\",\n      msg or \"assertion failed\", tostring(actual), type(actual)), 2)\n  end\nend\n\nfunction Assert.is_truthy(actual, msg)\n  if not actual then  -- truthy: anything not false\u002Fnil\n    error(string.format(\"%s: expected truthy, got %s\",\n      msg or \"assertion failed\", tostring(actual)), 2)\n  end\nend\n\nfunction Assert.raises(fn, expected_err_pattern, msg)\n  local ok, err = pcall(fn)\n  if ok then\n    error(msg or \"expected an error, but none was raised\", 2)\n  end\n  if expected_err_pattern and not tostring(err):match(expected_err_pattern) then\n    error(string.format(\"%s: error %q does not match %q\",\n      msg or \"assertion failed\", tostring(err), expected_err_pattern), 2)\n  end\nend\n\n-- Deep equality (for tables)\nfunction Assert.deep_equals(expected, actual, msg, seen)\n  seen = seen or {}\n  if type(expected) ~= type(actual) then\n    error(string.format(\"%s: type mismatch %s vs %s\",\n      msg or \"\", type(expected), type(actual)), 2)\n  end\n  if type(expected) ~= \"table\" then\n    if expected ~= actual then\n      error(string.format(\"%s: %s vs %s\",\n        msg or \"\", tostring(expected), tostring(actual)), 2)\n    end\n    return\n  end\n  if seen[expected] then return end  -- cycle\n  seen[expected] = true\n  for k, v in pairs(expected) do\n    Assert.deep_equals(v, actual[k], msg .. \".\" .. tostring(k), seen)\n  end\n  for k in pairs(actual) do\n    if expected[k] == nil then\n      error(string.format(\"%s: unexpected key %s\", msg or \"\", tostring(k)), 2)\n    end\n  end\nend\n\nreturn Assert\n","",[19,67,68,77,94,101,120,138,160,191,197,203,208,223,238,254,278,283,288,293,308,323,339,358,363,368,373,387,400,419,424,429,434,448,466,482,507,512,517,522,536,552,568,587,592,597,602,617,634,644,662,667,694,710,730,735,740,745,751,766,781,801,817,841,846,862,876,893,917,923,929,934,953,964,984,1008,1013,1029,1043,1075,1080,1085,1090,1095],{"__ignoreMap":65},[69,70,73],"span",{"class":71,"line":72},"line",1,[69,74,76],{"class":75},"sdCPZ","-- A minimal assertion library (no external deps) — 50 lines\n",[69,78,80,84,88,91],{"class":71,"line":79},2,[69,81,83],{"class":82},"svdQ7","local",[69,85,87],{"class":86},"ssxIu"," Assert ",[69,89,90],{"class":82},"=",[69,92,93],{"class":86}," {}\n",[69,95,97],{"class":71,"line":96},3,[69,98,100],{"emptyLinePlaceholder":99},true,"\n",[69,102,104,107,111,114,117],{"class":71,"line":103},4,[69,105,106],{"class":82},"function",[69,108,110],{"class":109},"sIsaT"," Assert",[69,112,113],{"class":86},".",[69,115,116],{"class":109},"equals",[69,118,119],{"class":86},"(expected, actual, msg)\n",[69,121,123,126,129,132,135],{"class":71,"line":122},5,[69,124,125],{"class":82},"  if",[69,127,128],{"class":86}," expected ",[69,130,131],{"class":82},"~=",[69,133,134],{"class":86}," actual ",[69,136,137],{"class":82},"then\n",[69,139,141,145,148,151,153,157],{"class":71,"line":140},6,[69,142,144],{"class":143},"snvgF","    error",[69,146,147],{"class":86},"(",[69,149,150],{"class":143},"string.format",[69,152,147],{"class":86},[69,154,156],{"class":155},"sJ6F3","\"%s: expected %s, got %s\"",[69,158,159],{"class":86},",\n",[69,161,163,166,169,172,174,177,180,182,185,188],{"class":71,"line":162},7,[69,164,165],{"class":86},"      msg ",[69,167,168],{"class":82},"or",[69,170,171],{"class":155}," \"assertion failed\"",[69,173,39],{"class":86},[69,175,176],{"class":143},"tostring",[69,178,179],{"class":86},"(expected), ",[69,181,176],{"class":143},[69,183,184],{"class":86},"(actual)), ",[69,186,187],{"class":143},"2",[69,189,190],{"class":86},")\n",[69,192,194],{"class":71,"line":193},8,[69,195,196],{"class":82},"  end\n",[69,198,200],{"class":71,"line":199},9,[69,201,202],{"class":82},"end\n",[69,204,206],{"class":71,"line":205},10,[69,207,100],{"emptyLinePlaceholder":99},[69,209,211,213,215,217,220],{"class":71,"line":210},11,[69,212,106],{"class":82},[69,214,110],{"class":109},[69,216,113],{"class":86},[69,218,219],{"class":109},"not_equals",[69,221,222],{"class":86},"(unexpected, actual, msg)\n",[69,224,226,228,231,234,236],{"class":71,"line":225},12,[69,227,125],{"class":82},[69,229,230],{"class":86}," unexpected ",[69,232,233],{"class":82},"==",[69,235,134],{"class":86},[69,237,137],{"class":82},[69,239,241,243,245,247,249,252],{"class":71,"line":240},13,[69,242,144],{"class":143},[69,244,147],{"class":86},[69,246,150],{"class":143},[69,248,147],{"class":86},[69,250,251],{"class":155},"\"%s: expected NOT %s, got %s\"",[69,253,159],{"class":86},[69,255,257,259,261,263,265,267,270,272,274,276],{"class":71,"line":256},14,[69,258,165],{"class":86},[69,260,168],{"class":82},[69,262,171],{"class":155},[69,264,39],{"class":86},[69,266,176],{"class":143},[69,268,269],{"class":86},"(unexpected), ",[69,271,176],{"class":143},[69,273,184],{"class":86},[69,275,187],{"class":143},[69,277,190],{"class":86},[69,279,281],{"class":71,"line":280},15,[69,282,196],{"class":82},[69,284,286],{"class":71,"line":285},16,[69,287,202],{"class":82},[69,289,291],{"class":71,"line":290},17,[69,292,100],{"emptyLinePlaceholder":99},[69,294,296,298,300,302,305],{"class":71,"line":295},18,[69,297,106],{"class":82},[69,299,110],{"class":109},[69,301,113],{"class":86},[69,303,304],{"class":109},"is_nil",[69,306,307],{"class":86},"(actual, msg)\n",[69,309,311,313,315,317,320],{"class":71,"line":310},19,[69,312,125],{"class":82},[69,314,134],{"class":86},[69,316,131],{"class":82},[69,318,319],{"class":143}," nil",[69,321,322],{"class":82}," then\n",[69,324,326,328,330,332,334,337],{"class":71,"line":325},20,[69,327,144],{"class":143},[69,329,147],{"class":86},[69,331,150],{"class":143},[69,333,147],{"class":86},[69,335,336],{"class":155},"\"%s: expected nil, got %s\"",[69,338,159],{"class":86},[69,340,342,344,346,348,350,352,354,356],{"class":71,"line":341},21,[69,343,165],{"class":86},[69,345,168],{"class":82},[69,347,171],{"class":155},[69,349,39],{"class":86},[69,351,176],{"class":143},[69,353,184],{"class":86},[69,355,187],{"class":143},[69,357,190],{"class":86},[69,359,361],{"class":71,"line":360},22,[69,362,196],{"class":82},[69,364,366],{"class":71,"line":365},23,[69,367,202],{"class":82},[69,369,371],{"class":71,"line":370},24,[69,372,100],{"emptyLinePlaceholder":99},[69,374,376,378,380,382,385],{"class":71,"line":375},25,[69,377,106],{"class":82},[69,379,110],{"class":109},[69,381,113],{"class":86},[69,383,384],{"class":109},"is_not_nil",[69,386,307],{"class":86},[69,388,390,392,394,396,398],{"class":71,"line":389},26,[69,391,125],{"class":82},[69,393,134],{"class":86},[69,395,233],{"class":82},[69,397,319],{"class":143},[69,399,322],{"class":82},[69,401,403,405,408,410,413,415,417],{"class":71,"line":402},27,[69,404,144],{"class":143},[69,406,407],{"class":86},"(msg ",[69,409,168],{"class":82},[69,411,412],{"class":155}," \"expected non-nil, got nil\"",[69,414,39],{"class":86},[69,416,187],{"class":143},[69,418,190],{"class":86},[69,420,422],{"class":71,"line":421},28,[69,423,196],{"class":82},[69,425,427],{"class":71,"line":426},29,[69,428,202],{"class":82},[69,430,432],{"class":71,"line":431},30,[69,433,100],{"emptyLinePlaceholder":99},[69,435,437,439,441,443,446],{"class":71,"line":436},31,[69,438,106],{"class":82},[69,440,110],{"class":109},[69,442,113],{"class":86},[69,444,445],{"class":109},"is_true",[69,447,307],{"class":86},[69,449,451,453,455,457,460,463],{"class":71,"line":450},32,[69,452,125],{"class":82},[69,454,134],{"class":86},[69,456,131],{"class":82},[69,458,459],{"class":143}," true",[69,461,462],{"class":82}," then",[69,464,465],{"class":75},"  -- STRICT: only `true`, not truthy\n",[69,467,469,471,473,475,477,480],{"class":71,"line":468},33,[69,470,144],{"class":143},[69,472,147],{"class":86},[69,474,150],{"class":143},[69,476,147],{"class":86},[69,478,479],{"class":155},"\"%s: expected true, got %s (%s)\"",[69,481,159],{"class":86},[69,483,485,487,489,491,493,495,498,501,503,505],{"class":71,"line":484},34,[69,486,165],{"class":86},[69,488,168],{"class":82},[69,490,171],{"class":155},[69,492,39],{"class":86},[69,494,176],{"class":143},[69,496,497],{"class":86},"(actual), ",[69,499,500],{"class":143},"type",[69,502,184],{"class":86},[69,504,187],{"class":143},[69,506,190],{"class":86},[69,508,510],{"class":71,"line":509},35,[69,511,196],{"class":82},[69,513,515],{"class":71,"line":514},36,[69,516,202],{"class":82},[69,518,520],{"class":71,"line":519},37,[69,521,100],{"emptyLinePlaceholder":99},[69,523,525,527,529,531,534],{"class":71,"line":524},38,[69,526,106],{"class":82},[69,528,110],{"class":109},[69,530,113],{"class":86},[69,532,533],{"class":109},"is_truthy",[69,535,307],{"class":86},[69,537,539,541,544,546,549],{"class":71,"line":538},39,[69,540,125],{"class":82},[69,542,543],{"class":82}," not",[69,545,134],{"class":86},[69,547,548],{"class":82},"then",[69,550,551],{"class":75},"  -- truthy: anything not false\u002Fnil\n",[69,553,555,557,559,561,563,566],{"class":71,"line":554},40,[69,556,144],{"class":143},[69,558,147],{"class":86},[69,560,150],{"class":143},[69,562,147],{"class":86},[69,564,565],{"class":155},"\"%s: expected truthy, got %s\"",[69,567,159],{"class":86},[69,569,571,573,575,577,579,581,583,585],{"class":71,"line":570},41,[69,572,165],{"class":86},[69,574,168],{"class":82},[69,576,171],{"class":155},[69,578,39],{"class":86},[69,580,176],{"class":143},[69,582,184],{"class":86},[69,584,187],{"class":143},[69,586,190],{"class":86},[69,588,590],{"class":71,"line":589},42,[69,591,196],{"class":82},[69,593,595],{"class":71,"line":594},43,[69,596,202],{"class":82},[69,598,600],{"class":71,"line":599},44,[69,601,100],{"emptyLinePlaceholder":99},[69,603,605,607,609,611,614],{"class":71,"line":604},45,[69,606,106],{"class":82},[69,608,110],{"class":109},[69,610,113],{"class":86},[69,612,613],{"class":109},"raises",[69,615,616],{"class":86},"(fn, expected_err_pattern, msg)\n",[69,618,620,623,626,628,631],{"class":71,"line":619},46,[69,621,622],{"class":82},"  local",[69,624,625],{"class":86}," ok, err ",[69,627,90],{"class":82},[69,629,630],{"class":143}," pcall",[69,632,633],{"class":86},"(fn)\n",[69,635,637,639,642],{"class":71,"line":636},47,[69,638,125],{"class":82},[69,640,641],{"class":86}," ok ",[69,643,137],{"class":82},[69,645,647,649,651,653,656,658,660],{"class":71,"line":646},48,[69,648,144],{"class":143},[69,650,407],{"class":86},[69,652,168],{"class":82},[69,654,655],{"class":155}," \"expected an error, but none was raised\"",[69,657,39],{"class":86},[69,659,187],{"class":143},[69,661,190],{"class":86},[69,663,665],{"class":71,"line":664},49,[69,666,196],{"class":82},[69,668,670,672,675,678,680,683,686,689,692],{"class":71,"line":669},50,[69,671,125],{"class":82},[69,673,674],{"class":86}," expected_err_pattern ",[69,676,677],{"class":82},"and",[69,679,543],{"class":82},[69,681,682],{"class":143}," tostring",[69,684,685],{"class":86},"(err):",[69,687,688],{"class":143},"match",[69,690,691],{"class":86},"(expected_err_pattern) ",[69,693,137],{"class":82},[69,695,697,699,701,703,705,708],{"class":71,"line":696},51,[69,698,144],{"class":143},[69,700,147],{"class":86},[69,702,150],{"class":143},[69,704,147],{"class":86},[69,706,707],{"class":155},"\"%s: error %q does not match %q\"",[69,709,159],{"class":86},[69,711,713,715,717,719,721,723,726,728],{"class":71,"line":712},52,[69,714,165],{"class":86},[69,716,168],{"class":82},[69,718,171],{"class":155},[69,720,39],{"class":86},[69,722,176],{"class":143},[69,724,725],{"class":86},"(err), expected_err_pattern), ",[69,727,187],{"class":143},[69,729,190],{"class":86},[69,731,733],{"class":71,"line":732},53,[69,734,196],{"class":82},[69,736,738],{"class":71,"line":737},54,[69,739,202],{"class":82},[69,741,743],{"class":71,"line":742},55,[69,744,100],{"emptyLinePlaceholder":99},[69,746,748],{"class":71,"line":747},56,[69,749,750],{"class":75},"-- Deep equality (for tables)\n",[69,752,754,756,758,760,763],{"class":71,"line":753},57,[69,755,106],{"class":82},[69,757,110],{"class":109},[69,759,113],{"class":86},[69,761,762],{"class":109},"deep_equals",[69,764,765],{"class":86},"(expected, actual, msg, seen)\n",[69,767,769,772,774,777,779],{"class":71,"line":768},58,[69,770,771],{"class":86},"  seen ",[69,773,90],{"class":82},[69,775,776],{"class":86}," seen ",[69,778,168],{"class":82},[69,780,93],{"class":86},[69,782,784,786,789,792,794,796,799],{"class":71,"line":783},59,[69,785,125],{"class":82},[69,787,788],{"class":143}," type",[69,790,791],{"class":86},"(expected) ",[69,793,131],{"class":82},[69,795,788],{"class":143},[69,797,798],{"class":86},"(actual) ",[69,800,137],{"class":82},[69,802,804,806,808,810,812,815],{"class":71,"line":803},60,[69,805,144],{"class":143},[69,807,147],{"class":86},[69,809,150],{"class":143},[69,811,147],{"class":86},[69,813,814],{"class":155},"\"%s: type mismatch %s vs %s\"",[69,816,159],{"class":86},[69,818,820,822,824,827,829,831,833,835,837,839],{"class":71,"line":819},61,[69,821,165],{"class":86},[69,823,168],{"class":82},[69,825,826],{"class":155}," \"\"",[69,828,39],{"class":86},[69,830,500],{"class":143},[69,832,179],{"class":86},[69,834,500],{"class":143},[69,836,184],{"class":86},[69,838,187],{"class":143},[69,840,190],{"class":86},[69,842,844],{"class":71,"line":843},62,[69,845,196],{"class":82},[69,847,849,851,853,855,857,860],{"class":71,"line":848},63,[69,850,125],{"class":82},[69,852,788],{"class":143},[69,854,791],{"class":86},[69,856,131],{"class":82},[69,858,859],{"class":155}," \"table\" ",[69,861,137],{"class":82},[69,863,865,868,870,872,874],{"class":71,"line":864},64,[69,866,867],{"class":82},"    if",[69,869,128],{"class":86},[69,871,131],{"class":82},[69,873,134],{"class":86},[69,875,137],{"class":82},[69,877,879,882,884,886,888,891],{"class":71,"line":878},65,[69,880,881],{"class":143},"      error",[69,883,147],{"class":86},[69,885,150],{"class":143},[69,887,147],{"class":86},[69,889,890],{"class":155},"\"%s: %s vs %s\"",[69,892,159],{"class":86},[69,894,896,899,901,903,905,907,909,911,913,915],{"class":71,"line":895},66,[69,897,898],{"class":86},"        msg ",[69,900,168],{"class":82},[69,902,826],{"class":155},[69,904,39],{"class":86},[69,906,176],{"class":143},[69,908,179],{"class":86},[69,910,176],{"class":143},[69,912,184],{"class":86},[69,914,187],{"class":143},[69,916,190],{"class":86},[69,918,920],{"class":71,"line":919},67,[69,921,922],{"class":82},"    end\n",[69,924,926],{"class":71,"line":925},68,[69,927,928],{"class":82},"    return\n",[69,930,932],{"class":71,"line":931},69,[69,933,196],{"class":82},[69,935,937,939,942,944,947,950],{"class":71,"line":936},70,[69,938,125],{"class":82},[69,940,941],{"class":86}," seen[expected] ",[69,943,548],{"class":82},[69,945,946],{"class":82}," return",[69,948,949],{"class":82}," end",[69,951,952],{"class":75},"  -- cycle\n",[69,954,956,959,961],{"class":71,"line":955},71,[69,957,958],{"class":86},"  seen[expected] ",[69,960,90],{"class":82},[69,962,963],{"class":143}," true\n",[69,965,967,970,973,976,979,981],{"class":71,"line":966},72,[69,968,969],{"class":82},"  for",[69,971,972],{"class":86}," k, v ",[69,974,975],{"class":82},"in",[69,977,978],{"class":143}," pairs",[69,980,791],{"class":86},[69,982,983],{"class":82},"do\n",[69,985,987,990,992,995,998,1001,1003,1005],{"class":71,"line":986},73,[69,988,989],{"class":86},"    Assert.",[69,991,762],{"class":143},[69,993,994],{"class":86},"(v, actual[k], msg ",[69,996,997],{"class":82},"..",[69,999,1000],{"class":155}," \".\" ",[69,1002,997],{"class":82},[69,1004,682],{"class":143},[69,1006,1007],{"class":86},"(k), seen)\n",[69,1009,1011],{"class":71,"line":1010},74,[69,1012,196],{"class":82},[69,1014,1016,1018,1021,1023,1025,1027],{"class":71,"line":1015},75,[69,1017,969],{"class":82},[69,1019,1020],{"class":86}," k ",[69,1022,975],{"class":82},[69,1024,978],{"class":143},[69,1026,798],{"class":86},[69,1028,983],{"class":82},[69,1030,1032,1034,1037,1039,1041],{"class":71,"line":1031},76,[69,1033,867],{"class":82},[69,1035,1036],{"class":86}," expected[k] ",[69,1038,233],{"class":82},[69,1040,319],{"class":143},[69,1042,322],{"class":82},[69,1044,1046,1048,1050,1052,1054,1057,1060,1062,1064,1066,1068,1071,1073],{"class":71,"line":1045},77,[69,1047,881],{"class":143},[69,1049,147],{"class":86},[69,1051,150],{"class":143},[69,1053,147],{"class":86},[69,1055,1056],{"class":155},"\"%s: unexpected key %s\"",[69,1058,1059],{"class":86},", msg ",[69,1061,168],{"class":82},[69,1063,826],{"class":155},[69,1065,39],{"class":86},[69,1067,176],{"class":143},[69,1069,1070],{"class":86},"(k)), ",[69,1072,187],{"class":143},[69,1074,190],{"class":86},[69,1076,1078],{"class":71,"line":1077},78,[69,1079,922],{"class":82},[69,1081,1083],{"class":71,"line":1082},79,[69,1084,196],{"class":82},[69,1086,1088],{"class":71,"line":1087},80,[69,1089,202],{"class":82},[69,1091,1093],{"class":71,"line":1092},81,[69,1094,100],{"emptyLinePlaceholder":99},[69,1096,1098,1101],{"class":71,"line":1097},82,[69,1099,1100],{"class":82},"return",[69,1102,1103],{"class":86}," Assert\n",[51,1105,1107],{"id":1106},"test-runner-with-setupteardown","Test Runner with Setup\u002FTeardown",[56,1109,1111,1845,1849,2529,2535,3360,3364],{"language":58,"filename":1110},"test\u002Frunner.lua",[61,1112,1114],{"className":63,"code":1113,"language":58,"meta":65,"style":65},"-- Minimal test runner: discovers test functions, runs with setup\u002Fteardown\nlocal Assert = require(\"assert\")\n\nlocal TestRunner = {}\nTestRunner.__index = TestRunner\n\nfunction TestRunner.new(name)\n  return setmetatable({\n    name = name,\n    passed = 0, failed = 0, errors = {},\n    setup_fn = nil, teardown_fn = nil,\n  }, TestRunner)\nend\n\nfunction TestRunner:setup(fn) self.setup_fn = fn; return self end\nfunction TestRunner:teardown(fn) self.teardown_fn = fn; return self end\n\nfunction TestRunner:test(name, fn)\n  local ctx = self.setup_fn and self.setup_fn() or {}\n  local ok, err = pcall(fn, ctx, Assert)\n  if self.teardown_fn then self.teardown_fn(ctx) end\n  if ok then\n    self.passed = self.passed + 1\n    io.write(\".\")\n  else\n    self.failed = self.failed + 1\n    self.errors[#self.errors + 1] = {name = name, err = err}\n    io.write(\"F\")\n  end\n  return self\nend\n\n-- Parametric tests: run the same test with multiple inputs\nfunction TestRunner:parametric(name, cases, fn)\n  for _, case in ipairs(cases) do\n    local case_name = string.format(\"%s [%s]\", name, case.desc or tostring(case.input))\n    self:test(case_name, function(ctx, A) fn(ctx, A, case.input, case.expected) end)\n  end\n  return self\nend\n\nfunction TestRunner:summary()\n  io.write(\"\\n\")\n  print(string.format(\"\\n%d passed, %d failed\", self.passed, self.failed))\n  for _, e in ipairs(self.errors) do\n    print(string.format(\"  FAIL: %s\\n    %s\", e.name, e.err))\n  end\n  return self.failed == 0\nend\n\nreturn TestRunner\n",[19,1115,1116,1121,1139,1143,1154,1168,1172,1187,1198,1208,1233,1251,1256,1260,1264,1300,1330,1334,1348,1379,1392,1415,1423,1447,1459,1464,1485,1529,1540,1544,1551,1555,1559,1564,1578,1595,1633,1671,1675,1681,1685,1689,1703,1720,1756,1779,1811,1815,1831,1835,1839],{"__ignoreMap":65},[69,1117,1118],{"class":71,"line":72},[69,1119,1120],{"class":75},"-- Minimal test runner: discovers test functions, runs with setup\u002Fteardown\n",[69,1122,1123,1125,1127,1129,1132,1134,1137],{"class":71,"line":79},[69,1124,83],{"class":82},[69,1126,87],{"class":86},[69,1128,90],{"class":82},[69,1130,1131],{"class":143}," require",[69,1133,147],{"class":86},[69,1135,1136],{"class":155},"\"assert\"",[69,1138,190],{"class":86},[69,1140,1141],{"class":71,"line":96},[69,1142,100],{"emptyLinePlaceholder":99},[69,1144,1145,1147,1150,1152],{"class":71,"line":103},[69,1146,83],{"class":82},[69,1148,1149],{"class":86}," TestRunner ",[69,1151,90],{"class":82},[69,1153,93],{"class":86},[69,1155,1156,1159,1162,1165],{"class":71,"line":122},[69,1157,1158],{"class":86},"TestRunner.",[69,1160,1161],{"class":109},"__index",[69,1163,1164],{"class":82}," =",[69,1166,1167],{"class":86}," TestRunner\n",[69,1169,1170],{"class":71,"line":140},[69,1171,100],{"emptyLinePlaceholder":99},[69,1173,1174,1176,1179,1181,1184],{"class":71,"line":162},[69,1175,106],{"class":82},[69,1177,1178],{"class":109}," TestRunner",[69,1180,113],{"class":86},[69,1182,1183],{"class":109},"new",[69,1185,1186],{"class":86},"(name)\n",[69,1188,1189,1192,1195],{"class":71,"line":193},[69,1190,1191],{"class":82},"  return",[69,1193,1194],{"class":143}," setmetatable",[69,1196,1197],{"class":86},"({\n",[69,1199,1200,1203,1205],{"class":71,"line":199},[69,1201,1202],{"class":86},"    name ",[69,1204,90],{"class":82},[69,1206,1207],{"class":86}," name,\n",[69,1209,1210,1213,1215,1218,1221,1223,1225,1228,1230],{"class":71,"line":205},[69,1211,1212],{"class":86},"    passed ",[69,1214,90],{"class":82},[69,1216,1217],{"class":143}," 0",[69,1219,1220],{"class":86},", failed ",[69,1222,90],{"class":82},[69,1224,1217],{"class":143},[69,1226,1227],{"class":86},", errors ",[69,1229,90],{"class":82},[69,1231,1232],{"class":86}," {},\n",[69,1234,1235,1238,1240,1242,1245,1247,1249],{"class":71,"line":210},[69,1236,1237],{"class":86},"    setup_fn ",[69,1239,90],{"class":82},[69,1241,319],{"class":143},[69,1243,1244],{"class":86},", teardown_fn ",[69,1246,90],{"class":82},[69,1248,319],{"class":143},[69,1250,159],{"class":86},[69,1252,1253],{"class":71,"line":225},[69,1254,1255],{"class":86},"  }, TestRunner)\n",[69,1257,1258],{"class":71,"line":240},[69,1259,202],{"class":82},[69,1261,1262],{"class":71,"line":256},[69,1263,100],{"emptyLinePlaceholder":99},[69,1265,1266,1268,1270,1273,1276,1279,1282,1284,1287,1289,1292,1294,1297],{"class":71,"line":280},[69,1267,106],{"class":82},[69,1269,1178],{"class":109},[69,1271,1272],{"class":86},":",[69,1274,1275],{"class":109},"setup",[69,1277,1278],{"class":86},"(fn) ",[69,1280,1281],{"class":143},"self",[69,1283,113],{"class":86},[69,1285,1286],{"class":109},"setup_fn",[69,1288,1164],{"class":82},[69,1290,1291],{"class":86}," fn; ",[69,1293,1100],{"class":82},[69,1295,1296],{"class":143}," self",[69,1298,1299],{"class":82}," end\n",[69,1301,1302,1304,1306,1308,1311,1313,1315,1317,1320,1322,1324,1326,1328],{"class":71,"line":285},[69,1303,106],{"class":82},[69,1305,1178],{"class":109},[69,1307,1272],{"class":86},[69,1309,1310],{"class":109},"teardown",[69,1312,1278],{"class":86},[69,1314,1281],{"class":143},[69,1316,113],{"class":86},[69,1318,1319],{"class":109},"teardown_fn",[69,1321,1164],{"class":82},[69,1323,1291],{"class":86},[69,1325,1100],{"class":82},[69,1327,1296],{"class":143},[69,1329,1299],{"class":82},[69,1331,1332],{"class":71,"line":290},[69,1333,100],{"emptyLinePlaceholder":99},[69,1335,1336,1338,1340,1342,1345],{"class":71,"line":295},[69,1337,106],{"class":82},[69,1339,1178],{"class":109},[69,1341,1272],{"class":86},[69,1343,1344],{"class":109},"test",[69,1346,1347],{"class":86},"(name, fn)\n",[69,1349,1350,1352,1355,1357,1359,1361,1363,1366,1368,1370,1372,1375,1377],{"class":71,"line":310},[69,1351,622],{"class":82},[69,1353,1354],{"class":86}," ctx ",[69,1356,90],{"class":82},[69,1358,1296],{"class":143},[69,1360,113],{"class":86},[69,1362,1286],{"class":109},[69,1364,1365],{"class":82}," and",[69,1367,1296],{"class":143},[69,1369,113],{"class":86},[69,1371,1286],{"class":143},[69,1373,1374],{"class":86},"() ",[69,1376,168],{"class":82},[69,1378,93],{"class":86},[69,1380,1381,1383,1385,1387,1389],{"class":71,"line":325},[69,1382,622],{"class":82},[69,1384,625],{"class":86},[69,1386,90],{"class":82},[69,1388,630],{"class":143},[69,1390,1391],{"class":86},"(fn, ctx, Assert)\n",[69,1393,1394,1396,1398,1400,1402,1404,1406,1408,1410,1413],{"class":71,"line":341},[69,1395,125],{"class":82},[69,1397,1296],{"class":143},[69,1399,113],{"class":86},[69,1401,1319],{"class":109},[69,1403,462],{"class":82},[69,1405,1296],{"class":143},[69,1407,113],{"class":86},[69,1409,1319],{"class":143},[69,1411,1412],{"class":86},"(ctx) ",[69,1414,202],{"class":82},[69,1416,1417,1419,1421],{"class":71,"line":360},[69,1418,125],{"class":82},[69,1420,641],{"class":86},[69,1422,137],{"class":82},[69,1424,1425,1428,1430,1433,1435,1437,1439,1441,1444],{"class":71,"line":365},[69,1426,1427],{"class":143},"    self",[69,1429,113],{"class":86},[69,1431,1432],{"class":109},"passed",[69,1434,1164],{"class":82},[69,1436,1296],{"class":143},[69,1438,113],{"class":86},[69,1440,1432],{"class":109},[69,1442,1443],{"class":82}," +",[69,1445,1446],{"class":143}," 1\n",[69,1448,1449,1452,1454,1457],{"class":71,"line":370},[69,1450,1451],{"class":143},"    io.write",[69,1453,147],{"class":86},[69,1455,1456],{"class":155},"\".\"",[69,1458,190],{"class":86},[69,1460,1461],{"class":71,"line":375},[69,1462,1463],{"class":82},"  else\n",[69,1465,1466,1468,1470,1473,1475,1477,1479,1481,1483],{"class":71,"line":389},[69,1467,1427],{"class":143},[69,1469,113],{"class":86},[69,1471,1472],{"class":109},"failed",[69,1474,1164],{"class":82},[69,1476,1296],{"class":143},[69,1478,113],{"class":86},[69,1480,1472],{"class":109},[69,1482,1443],{"class":82},[69,1484,1446],{"class":143},[69,1486,1487,1489,1491,1494,1497,1500,1502,1504,1506,1508,1511,1514,1516,1519,1521,1524,1526],{"class":71,"line":402},[69,1488,1427],{"class":143},[69,1490,113],{"class":86},[69,1492,1493],{"class":109},"errors",[69,1495,1496],{"class":86},"[",[69,1498,1499],{"class":82},"#",[69,1501,1281],{"class":143},[69,1503,113],{"class":86},[69,1505,1493],{"class":109},[69,1507,1443],{"class":82},[69,1509,1510],{"class":143}," 1",[69,1512,1513],{"class":86},"] ",[69,1515,90],{"class":82},[69,1517,1518],{"class":86}," {name ",[69,1520,90],{"class":82},[69,1522,1523],{"class":86}," name, err ",[69,1525,90],{"class":82},[69,1527,1528],{"class":86}," err}\n",[69,1530,1531,1533,1535,1538],{"class":71,"line":421},[69,1532,1451],{"class":143},[69,1534,147],{"class":86},[69,1536,1537],{"class":155},"\"F\"",[69,1539,190],{"class":86},[69,1541,1542],{"class":71,"line":426},[69,1543,196],{"class":82},[69,1545,1546,1548],{"class":71,"line":431},[69,1547,1191],{"class":82},[69,1549,1550],{"class":143}," self\n",[69,1552,1553],{"class":71,"line":436},[69,1554,202],{"class":82},[69,1556,1557],{"class":71,"line":450},[69,1558,100],{"emptyLinePlaceholder":99},[69,1560,1561],{"class":71,"line":468},[69,1562,1563],{"class":75},"-- Parametric tests: run the same test with multiple inputs\n",[69,1565,1566,1568,1570,1572,1575],{"class":71,"line":484},[69,1567,106],{"class":82},[69,1569,1178],{"class":109},[69,1571,1272],{"class":86},[69,1573,1574],{"class":109},"parametric",[69,1576,1577],{"class":86},"(name, cases, fn)\n",[69,1579,1580,1582,1585,1587,1590,1593],{"class":71,"line":509},[69,1581,969],{"class":82},[69,1583,1584],{"class":86}," _, case ",[69,1586,975],{"class":82},[69,1588,1589],{"class":143}," ipairs",[69,1591,1592],{"class":86},"(cases) ",[69,1594,983],{"class":82},[69,1596,1597,1600,1603,1605,1608,1610,1613,1616,1619,1622,1624,1627,1630],{"class":71,"line":514},[69,1598,1599],{"class":82},"    local",[69,1601,1602],{"class":86}," case_name ",[69,1604,90],{"class":82},[69,1606,1607],{"class":143}," string.format",[69,1609,147],{"class":86},[69,1611,1612],{"class":155},"\"%s [%s]\"",[69,1614,1615],{"class":86},", name, case.",[69,1617,1618],{"class":109},"desc",[69,1620,1621],{"class":82}," or",[69,1623,682],{"class":143},[69,1625,1626],{"class":86},"(case.",[69,1628,1629],{"class":109},"input",[69,1631,1632],{"class":86},"))\n",[69,1634,1635,1637,1639,1641,1644,1646,1649,1652,1655,1657,1660,1663,1666,1669],{"class":71,"line":519},[69,1636,1427],{"class":143},[69,1638,1272],{"class":86},[69,1640,1344],{"class":143},[69,1642,1643],{"class":86},"(case_name, ",[69,1645,106],{"class":82},[69,1647,1648],{"class":86},"(ctx, A) ",[69,1650,1651],{"class":143},"fn",[69,1653,1654],{"class":86},"(ctx, A, case.",[69,1656,1629],{"class":109},[69,1658,1659],{"class":86},", case.",[69,1661,1662],{"class":109},"expected",[69,1664,1665],{"class":86},") ",[69,1667,1668],{"class":82},"end",[69,1670,190],{"class":86},[69,1672,1673],{"class":71,"line":524},[69,1674,196],{"class":82},[69,1676,1677,1679],{"class":71,"line":538},[69,1678,1191],{"class":82},[69,1680,1550],{"class":143},[69,1682,1683],{"class":71,"line":554},[69,1684,202],{"class":82},[69,1686,1687],{"class":71,"line":570},[69,1688,100],{"emptyLinePlaceholder":99},[69,1690,1691,1693,1695,1697,1700],{"class":71,"line":589},[69,1692,106],{"class":82},[69,1694,1178],{"class":109},[69,1696,1272],{"class":86},[69,1698,1699],{"class":109},"summary",[69,1701,1702],{"class":86},"()\n",[69,1704,1705,1708,1710,1713,1716,1718],{"class":71,"line":594},[69,1706,1707],{"class":143},"  io.write",[69,1709,147],{"class":86},[69,1711,1712],{"class":155},"\"",[69,1714,1715],{"class":143},"\\n",[69,1717,1712],{"class":155},[69,1719,190],{"class":86},[69,1721,1722,1725,1727,1729,1731,1733,1735,1738,1740,1742,1744,1746,1748,1750,1752,1754],{"class":71,"line":599},[69,1723,1724],{"class":143},"  print",[69,1726,147],{"class":86},[69,1728,150],{"class":143},[69,1730,147],{"class":86},[69,1732,1712],{"class":155},[69,1734,1715],{"class":143},[69,1736,1737],{"class":155},"%d passed, %d failed\"",[69,1739,39],{"class":86},[69,1741,1281],{"class":143},[69,1743,113],{"class":86},[69,1745,1432],{"class":109},[69,1747,39],{"class":86},[69,1749,1281],{"class":143},[69,1751,113],{"class":86},[69,1753,1472],{"class":109},[69,1755,1632],{"class":86},[69,1757,1758,1760,1763,1765,1767,1769,1771,1773,1775,1777],{"class":71,"line":604},[69,1759,969],{"class":82},[69,1761,1762],{"class":86}," _, e ",[69,1764,975],{"class":82},[69,1766,1589],{"class":143},[69,1768,147],{"class":86},[69,1770,1281],{"class":143},[69,1772,113],{"class":86},[69,1774,1493],{"class":109},[69,1776,1665],{"class":86},[69,1778,983],{"class":82},[69,1780,1781,1784,1786,1788,1790,1793,1795,1798,1801,1804,1806,1809],{"class":71,"line":619},[69,1782,1783],{"class":143},"    print",[69,1785,147],{"class":86},[69,1787,150],{"class":143},[69,1789,147],{"class":86},[69,1791,1792],{"class":155},"\"  FAIL: %s",[69,1794,1715],{"class":143},[69,1796,1797],{"class":155},"    %s\"",[69,1799,1800],{"class":86},", e.",[69,1802,1803],{"class":109},"name",[69,1805,1800],{"class":86},[69,1807,1808],{"class":109},"err",[69,1810,1632],{"class":86},[69,1812,1813],{"class":71,"line":636},[69,1814,196],{"class":82},[69,1816,1817,1819,1821,1823,1825,1828],{"class":71,"line":646},[69,1818,1191],{"class":82},[69,1820,1296],{"class":143},[69,1822,113],{"class":86},[69,1824,1472],{"class":109},[69,1826,1827],{"class":82}," ==",[69,1829,1830],{"class":143}," 0\n",[69,1832,1833],{"class":71,"line":664},[69,1834,202],{"class":82},[69,1836,1837],{"class":71,"line":669},[69,1838,100],{"emptyLinePlaceholder":99},[69,1840,1841,1843],{"class":71,"line":696},[69,1842,1100],{"class":82},[69,1844,1167],{"class":86},[51,1846,1848],{"id":1847},"table-based-tests-parametric","Table-Based Tests (Parametric)",[56,1850,1852],{"language":58,"filename":1851},"test\u002Ftest_string_utils.lua",[61,1853,1855],{"className":63,"code":1854,"language":58,"meta":65,"style":65},"local Runner = require(\"runner\")\nlocal A = require(\"assert\")\nlocal su = require(\"string_utils\")  -- module under test\n\nlocal T = Runner.new(\"string_utils\")\n\n-- Parametric: run many inputs through the same test logic\nT:parametric(\"trim\", {\n  {desc = \"both sides\",   input = \"  hello  \",  expected = \"hello\"},\n  {desc = \"leading only\", input = \"hello  \",    expected = \"hello\"},\n  {desc = \"trailing only\",input = \"  hello\",    expected = \"hello\"},\n  {desc = \"no trim\",      input = \"hello\",     expected = \"hello\"},\n  {desc = \"empty\",        input = \"\",           expected = \"\"},\n  {desc = \"all spaces\",   input = \"   \",        expected = \"\"},\n  {desc = \"tabs\",         input = \"\\thello\\t\", expected = \"hello\"},\n}, function(ctx, A, input, expected)\n  A.equals(expected, su.trim(input))\nend)\n\n-- Edge cases as explicit tests\nT:test(\"split: empty string\", function(ctx, A)\n  A.deep_equals({\"\"}, su.split(\"\", \",\"))\nend)\n\nT:test(\"split: single delimiter\", function(ctx, A)\n  A.deep_equals({\"a\", \"b\", \"c\"}, su.split(\"a,b,c\", \",\"))\nend)\n\nT:test(\"split: trailing delimiter\", function(ctx, A)\n  A.deep_equals({\"a\", \"b\", \"c\", \"\"}, su.split(\"a,b,c,\", \",\"))\nend)\n\nT:test(\"split: no delimiter in string\", function(ctx, A)\n  A.deep_equals({\"hello\"}, su.split(\"hello\", \",\"))\nend)\n\nT:test(\"capitalize: handles nil\", function(ctx, A)\n  A.raises(function() su.capitalize(nil) end, \"expected string\")\nend)\n\nos.exit(T:summary() and 0 or 1)  -- exit code for CI\n",[19,1856,1857,1875,1892,1914,1918,1938,1942,1947,1964,1993,2019,2044,2069,2094,2119,2155,2165,2181,2187,2191,2196,2216,2245,2251,2255,2274,2310,2316,2320,2339,2376,2382,2386,2405,2430,2436,2440,2459,2491,2497,2501],{"__ignoreMap":65},[69,1858,1859,1861,1864,1866,1868,1870,1873],{"class":71,"line":72},[69,1860,83],{"class":82},[69,1862,1863],{"class":86}," Runner ",[69,1865,90],{"class":82},[69,1867,1131],{"class":143},[69,1869,147],{"class":86},[69,1871,1872],{"class":155},"\"runner\"",[69,1874,190],{"class":86},[69,1876,1877,1879,1882,1884,1886,1888,1890],{"class":71,"line":79},[69,1878,83],{"class":82},[69,1880,1881],{"class":86}," A ",[69,1883,90],{"class":82},[69,1885,1131],{"class":143},[69,1887,147],{"class":86},[69,1889,1136],{"class":155},[69,1891,190],{"class":86},[69,1893,1894,1896,1899,1901,1903,1905,1908,1911],{"class":71,"line":96},[69,1895,83],{"class":82},[69,1897,1898],{"class":86}," su ",[69,1900,90],{"class":82},[69,1902,1131],{"class":143},[69,1904,147],{"class":86},[69,1906,1907],{"class":155},"\"string_utils\"",[69,1909,1910],{"class":86},")  ",[69,1912,1913],{"class":75},"-- module under test\n",[69,1915,1916],{"class":71,"line":103},[69,1917,100],{"emptyLinePlaceholder":99},[69,1919,1920,1922,1925,1927,1930,1932,1934,1936],{"class":71,"line":122},[69,1921,83],{"class":82},[69,1923,1924],{"class":86}," T ",[69,1926,90],{"class":82},[69,1928,1929],{"class":86}," Runner.",[69,1931,1183],{"class":143},[69,1933,147],{"class":86},[69,1935,1907],{"class":155},[69,1937,190],{"class":86},[69,1939,1940],{"class":71,"line":140},[69,1941,100],{"emptyLinePlaceholder":99},[69,1943,1944],{"class":71,"line":162},[69,1945,1946],{"class":75},"-- Parametric: run many inputs through the same test logic\n",[69,1948,1949,1952,1954,1956,1958,1961],{"class":71,"line":193},[69,1950,1951],{"class":109},"T",[69,1953,1272],{"class":86},[69,1955,1574],{"class":143},[69,1957,147],{"class":86},[69,1959,1960],{"class":155},"\"trim\"",[69,1962,1963],{"class":86},", {\n",[69,1965,1966,1969,1971,1974,1977,1979,1982,1985,1987,1990],{"class":71,"line":199},[69,1967,1968],{"class":86},"  {desc ",[69,1970,90],{"class":82},[69,1972,1973],{"class":155}," \"both sides\"",[69,1975,1976],{"class":86},",   input ",[69,1978,90],{"class":82},[69,1980,1981],{"class":155}," \"  hello  \"",[69,1983,1984],{"class":86},",  expected ",[69,1986,90],{"class":82},[69,1988,1989],{"class":155}," \"hello\"",[69,1991,1992],{"class":86},"},\n",[69,1994,1995,1997,1999,2002,2005,2007,2010,2013,2015,2017],{"class":71,"line":205},[69,1996,1968],{"class":86},[69,1998,90],{"class":82},[69,2000,2001],{"class":155}," \"leading only\"",[69,2003,2004],{"class":86},", input ",[69,2006,90],{"class":82},[69,2008,2009],{"class":155}," \"hello  \"",[69,2011,2012],{"class":86},",    expected ",[69,2014,90],{"class":82},[69,2016,1989],{"class":155},[69,2018,1992],{"class":86},[69,2020,2021,2023,2025,2028,2031,2033,2036,2038,2040,2042],{"class":71,"line":210},[69,2022,1968],{"class":86},[69,2024,90],{"class":82},[69,2026,2027],{"class":155}," \"trailing only\"",[69,2029,2030],{"class":86},",input ",[69,2032,90],{"class":82},[69,2034,2035],{"class":155}," \"  hello\"",[69,2037,2012],{"class":86},[69,2039,90],{"class":82},[69,2041,1989],{"class":155},[69,2043,1992],{"class":86},[69,2045,2046,2048,2050,2053,2056,2058,2060,2063,2065,2067],{"class":71,"line":225},[69,2047,1968],{"class":86},[69,2049,90],{"class":82},[69,2051,2052],{"class":155}," \"no trim\"",[69,2054,2055],{"class":86},",      input ",[69,2057,90],{"class":82},[69,2059,1989],{"class":155},[69,2061,2062],{"class":86},",     expected ",[69,2064,90],{"class":82},[69,2066,1989],{"class":155},[69,2068,1992],{"class":86},[69,2070,2071,2073,2075,2078,2081,2083,2085,2088,2090,2092],{"class":71,"line":240},[69,2072,1968],{"class":86},[69,2074,90],{"class":82},[69,2076,2077],{"class":155}," \"empty\"",[69,2079,2080],{"class":86},",        input ",[69,2082,90],{"class":82},[69,2084,826],{"class":155},[69,2086,2087],{"class":86},",           expected ",[69,2089,90],{"class":82},[69,2091,826],{"class":155},[69,2093,1992],{"class":86},[69,2095,2096,2098,2100,2103,2105,2107,2110,2113,2115,2117],{"class":71,"line":256},[69,2097,1968],{"class":86},[69,2099,90],{"class":82},[69,2101,2102],{"class":155}," \"all spaces\"",[69,2104,1976],{"class":86},[69,2106,90],{"class":82},[69,2108,2109],{"class":155}," \"   \"",[69,2111,2112],{"class":86},",        expected ",[69,2114,90],{"class":82},[69,2116,826],{"class":155},[69,2118,1992],{"class":86},[69,2120,2121,2123,2125,2128,2131,2133,2136,2139,2142,2144,2146,2149,2151,2153],{"class":71,"line":280},[69,2122,1968],{"class":86},[69,2124,90],{"class":82},[69,2126,2127],{"class":155}," \"tabs\"",[69,2129,2130],{"class":86},",         input ",[69,2132,90],{"class":82},[69,2134,2135],{"class":155}," \"",[69,2137,2138],{"class":143},"\\t",[69,2140,2141],{"class":155},"hello",[69,2143,2138],{"class":143},[69,2145,1712],{"class":155},[69,2147,2148],{"class":86},", expected ",[69,2150,90],{"class":82},[69,2152,1989],{"class":155},[69,2154,1992],{"class":86},[69,2156,2157,2160,2162],{"class":71,"line":285},[69,2158,2159],{"class":86},"}, ",[69,2161,106],{"class":82},[69,2163,2164],{"class":86},"(ctx, A, input, expected)\n",[69,2166,2167,2170,2172,2175,2178],{"class":71,"line":290},[69,2168,2169],{"class":86},"  A.",[69,2171,116],{"class":143},[69,2173,2174],{"class":86},"(expected, su.",[69,2176,2177],{"class":143},"trim",[69,2179,2180],{"class":86},"(input))\n",[69,2182,2183,2185],{"class":71,"line":295},[69,2184,1668],{"class":82},[69,2186,190],{"class":86},[69,2188,2189],{"class":71,"line":310},[69,2190,100],{"emptyLinePlaceholder":99},[69,2192,2193],{"class":71,"line":325},[69,2194,2195],{"class":75},"-- Edge cases as explicit tests\n",[69,2197,2198,2200,2202,2204,2206,2209,2211,2213],{"class":71,"line":341},[69,2199,1951],{"class":109},[69,2201,1272],{"class":86},[69,2203,1344],{"class":143},[69,2205,147],{"class":86},[69,2207,2208],{"class":155},"\"split: empty string\"",[69,2210,39],{"class":86},[69,2212,106],{"class":82},[69,2214,2215],{"class":86},"(ctx, A)\n",[69,2217,2218,2220,2222,2225,2228,2231,2234,2236,2238,2240,2243],{"class":71,"line":360},[69,2219,2169],{"class":86},[69,2221,762],{"class":143},[69,2223,2224],{"class":86},"({",[69,2226,2227],{"class":155},"\"\"",[69,2229,2230],{"class":86},"}, su.",[69,2232,2233],{"class":143},"split",[69,2235,147],{"class":86},[69,2237,2227],{"class":155},[69,2239,39],{"class":86},[69,2241,2242],{"class":155},"\",\"",[69,2244,1632],{"class":86},[69,2246,2247,2249],{"class":71,"line":365},[69,2248,1668],{"class":82},[69,2250,190],{"class":86},[69,2252,2253],{"class":71,"line":370},[69,2254,100],{"emptyLinePlaceholder":99},[69,2256,2257,2259,2261,2263,2265,2268,2270,2272],{"class":71,"line":375},[69,2258,1951],{"class":109},[69,2260,1272],{"class":86},[69,2262,1344],{"class":143},[69,2264,147],{"class":86},[69,2266,2267],{"class":155},"\"split: single delimiter\"",[69,2269,39],{"class":86},[69,2271,106],{"class":82},[69,2273,2215],{"class":86},[69,2275,2276,2278,2280,2282,2285,2287,2290,2292,2295,2297,2299,2301,2304,2306,2308],{"class":71,"line":389},[69,2277,2169],{"class":86},[69,2279,762],{"class":143},[69,2281,2224],{"class":86},[69,2283,2284],{"class":155},"\"a\"",[69,2286,39],{"class":86},[69,2288,2289],{"class":155},"\"b\"",[69,2291,39],{"class":86},[69,2293,2294],{"class":155},"\"c\"",[69,2296,2230],{"class":86},[69,2298,2233],{"class":143},[69,2300,147],{"class":86},[69,2302,2303],{"class":155},"\"a,b,c\"",[69,2305,39],{"class":86},[69,2307,2242],{"class":155},[69,2309,1632],{"class":86},[69,2311,2312,2314],{"class":71,"line":402},[69,2313,1668],{"class":82},[69,2315,190],{"class":86},[69,2317,2318],{"class":71,"line":421},[69,2319,100],{"emptyLinePlaceholder":99},[69,2321,2322,2324,2326,2328,2330,2333,2335,2337],{"class":71,"line":426},[69,2323,1951],{"class":109},[69,2325,1272],{"class":86},[69,2327,1344],{"class":143},[69,2329,147],{"class":86},[69,2331,2332],{"class":155},"\"split: trailing delimiter\"",[69,2334,39],{"class":86},[69,2336,106],{"class":82},[69,2338,2215],{"class":86},[69,2340,2341,2343,2345,2347,2349,2351,2353,2355,2357,2359,2361,2363,2365,2367,2370,2372,2374],{"class":71,"line":431},[69,2342,2169],{"class":86},[69,2344,762],{"class":143},[69,2346,2224],{"class":86},[69,2348,2284],{"class":155},[69,2350,39],{"class":86},[69,2352,2289],{"class":155},[69,2354,39],{"class":86},[69,2356,2294],{"class":155},[69,2358,39],{"class":86},[69,2360,2227],{"class":155},[69,2362,2230],{"class":86},[69,2364,2233],{"class":143},[69,2366,147],{"class":86},[69,2368,2369],{"class":155},"\"a,b,c,\"",[69,2371,39],{"class":86},[69,2373,2242],{"class":155},[69,2375,1632],{"class":86},[69,2377,2378,2380],{"class":71,"line":436},[69,2379,1668],{"class":82},[69,2381,190],{"class":86},[69,2383,2384],{"class":71,"line":450},[69,2385,100],{"emptyLinePlaceholder":99},[69,2387,2388,2390,2392,2394,2396,2399,2401,2403],{"class":71,"line":468},[69,2389,1951],{"class":109},[69,2391,1272],{"class":86},[69,2393,1344],{"class":143},[69,2395,147],{"class":86},[69,2397,2398],{"class":155},"\"split: no delimiter in string\"",[69,2400,39],{"class":86},[69,2402,106],{"class":82},[69,2404,2215],{"class":86},[69,2406,2407,2409,2411,2413,2416,2418,2420,2422,2424,2426,2428],{"class":71,"line":484},[69,2408,2169],{"class":86},[69,2410,762],{"class":143},[69,2412,2224],{"class":86},[69,2414,2415],{"class":155},"\"hello\"",[69,2417,2230],{"class":86},[69,2419,2233],{"class":143},[69,2421,147],{"class":86},[69,2423,2415],{"class":155},[69,2425,39],{"class":86},[69,2427,2242],{"class":155},[69,2429,1632],{"class":86},[69,2431,2432,2434],{"class":71,"line":509},[69,2433,1668],{"class":82},[69,2435,190],{"class":86},[69,2437,2438],{"class":71,"line":514},[69,2439,100],{"emptyLinePlaceholder":99},[69,2441,2442,2444,2446,2448,2450,2453,2455,2457],{"class":71,"line":519},[69,2443,1951],{"class":109},[69,2445,1272],{"class":86},[69,2447,1344],{"class":143},[69,2449,147],{"class":86},[69,2451,2452],{"class":155},"\"capitalize: handles nil\"",[69,2454,39],{"class":86},[69,2456,106],{"class":82},[69,2458,2215],{"class":86},[69,2460,2461,2463,2465,2467,2469,2472,2475,2477,2480,2482,2484,2486,2489],{"class":71,"line":524},[69,2462,2169],{"class":86},[69,2464,613],{"class":143},[69,2466,147],{"class":86},[69,2468,106],{"class":82},[69,2470,2471],{"class":86},"() su.",[69,2473,2474],{"class":143},"capitalize",[69,2476,147],{"class":86},[69,2478,2479],{"class":143},"nil",[69,2481,1665],{"class":86},[69,2483,1668],{"class":82},[69,2485,39],{"class":86},[69,2487,2488],{"class":155},"\"expected string\"",[69,2490,190],{"class":86},[69,2492,2493,2495],{"class":71,"line":538},[69,2494,1668],{"class":82},[69,2496,190],{"class":86},[69,2498,2499],{"class":71,"line":554},[69,2500,100],{"emptyLinePlaceholder":99},[69,2502,2503,2506,2508,2510,2512,2514,2516,2518,2520,2522,2524,2526],{"class":71,"line":570},[69,2504,2505],{"class":143},"os.exit",[69,2507,147],{"class":86},[69,2509,1951],{"class":109},[69,2511,1272],{"class":86},[69,2513,1699],{"class":143},[69,2515,1374],{"class":86},[69,2517,677],{"class":82},[69,2519,1217],{"class":143},[69,2521,1621],{"class":82},[69,2523,1510],{"class":143},[69,2525,1910],{"class":86},[69,2527,2528],{"class":75},"-- exit code for CI\n",[51,2530,2532,2533],{"id":2531},"mocking-with-packagepreload","Mocking with ",[19,2534,44],{},[56,2536,2537],{"language":58},[61,2538,2540],{"className":63,"code":2539,"language":58,"meta":65,"style":65},"-- Mock a module BEFORE it's required by the code under test\n-- package.preload[name] = function() return mock_module end\n\n-- test\u002Ftest_user_service.lua\nlocal A = require(\"assert\")\n\n-- Inject a mock database before the service requires it\npackage.preload[\"db\"] = function()\n  local users = {\n    [1] = {id = 1, name = \"Alice\"},\n    [2] = {id = 2, name = \"Bob\"},\n  }\n  return {\n    find = function(id) return users[id] end,\n    save = function(user) users[user.id] = user; return true end,\n    delete = function(id) users[id] = nil; return true end,\n    -- Track calls for verification\n    calls = {find = 0, save = 0, delete = 0},\n  }\nend\n\n-- Set _ENV or modify the mock to track calls:\nlocal mock_db\npackage.preload[\"db\"] = function()\n  mock_db = {\n    _users = {[1] = {id = 1, name = \"Alice\"}},\n    _calls = {},\n    find = function(self, id)\n      self._calls.find = (self._calls.find or 0) + 1\n      return self._users[id]\n    end,\n    save = function(self, user)\n      self._calls.save = (self._calls.save or 0) + 1\n      self._users[user.id] = user\n      return true\n    end,\n  }\n  -- Return a table that auto-passes self (method syntax)\n  return setmetatable({}, {\n    __index = function(_, k)\n      if k == \"calls\" then return mock_db._calls end\n      return function(...) return mock_db[k](mock_db, ...) end\n    end\n  })\nend\n\n-- NOW require the service — it gets the mock db\nlocal UserService = require(\"user_service\")\n\nlocal T = require(\"runner\").new(\"UserService\")\n\nT:test(\"get_user returns user from db\", function(ctx, A)\n  local user = UserService.get_user(1)\n  A.deep_equals({id = 1, name = \"Alice\"}, user)\nend)\n\nT:test(\"save_user calls db.save\", function(ctx, A)\n  UserService.save_user({id = 3, name = \"Charlie\"})\n  A.is_not_nil(mock_db._calls.save)\n  A.equals(1, mock_db._calls.save)\nend)\n\nT:summary()\n",[19,2541,2542,2547,2552,2556,2561,2577,2581,2586,2604,2616,2645,2671,2676,2682,2703,2733,2760,2765,2795,2799,2803,2807,2812,2819,2835,2844,2875,2884,2895,2936,2951,2958,2969,3006,3026,3032,3038,3042,3047,3056,3068,3091,3112,3116,3121,3125,3129,3134,3152,3156,3182,3186,3205,3226,3248,3254,3258,3277,3302,3319,3340,3346,3350],{"__ignoreMap":65},[69,2543,2544],{"class":71,"line":72},[69,2545,2546],{"class":75},"-- Mock a module BEFORE it's required by the code under test\n",[69,2548,2549],{"class":71,"line":79},[69,2550,2551],{"class":75},"-- package.preload[name] = function() return mock_module end\n",[69,2553,2554],{"class":71,"line":96},[69,2555,100],{"emptyLinePlaceholder":99},[69,2557,2558],{"class":71,"line":103},[69,2559,2560],{"class":75},"-- test\u002Ftest_user_service.lua\n",[69,2562,2563,2565,2567,2569,2571,2573,2575],{"class":71,"line":122},[69,2564,83],{"class":82},[69,2566,1881],{"class":86},[69,2568,90],{"class":82},[69,2570,1131],{"class":143},[69,2572,147],{"class":86},[69,2574,1136],{"class":155},[69,2576,190],{"class":86},[69,2578,2579],{"class":71,"line":140},[69,2580,100],{"emptyLinePlaceholder":99},[69,2582,2583],{"class":71,"line":162},[69,2584,2585],{"class":75},"-- Inject a mock database before the service requires it\n",[69,2587,2588,2590,2592,2595,2597,2599,2602],{"class":71,"line":193},[69,2589,44],{"class":143},[69,2591,1496],{"class":86},[69,2593,2594],{"class":155},"\"db\"",[69,2596,1513],{"class":86},[69,2598,90],{"class":82},[69,2600,2601],{"class":82}," function",[69,2603,1702],{"class":86},[69,2605,2606,2608,2611,2613],{"class":71,"line":199},[69,2607,622],{"class":82},[69,2609,2610],{"class":86}," users ",[69,2612,90],{"class":82},[69,2614,2615],{"class":86}," {\n",[69,2617,2618,2621,2624,2626,2628,2631,2633,2635,2638,2640,2643],{"class":71,"line":205},[69,2619,2620],{"class":86},"    [",[69,2622,2623],{"class":143},"1",[69,2625,1513],{"class":86},[69,2627,90],{"class":82},[69,2629,2630],{"class":86}," {id ",[69,2632,90],{"class":82},[69,2634,1510],{"class":143},[69,2636,2637],{"class":86},", name ",[69,2639,90],{"class":82},[69,2641,2642],{"class":155}," \"Alice\"",[69,2644,1992],{"class":86},[69,2646,2647,2649,2651,2653,2655,2657,2659,2662,2664,2666,2669],{"class":71,"line":210},[69,2648,2620],{"class":86},[69,2650,187],{"class":143},[69,2652,1513],{"class":86},[69,2654,90],{"class":82},[69,2656,2630],{"class":86},[69,2658,90],{"class":82},[69,2660,2661],{"class":143}," 2",[69,2663,2637],{"class":86},[69,2665,90],{"class":82},[69,2667,2668],{"class":155}," \"Bob\"",[69,2670,1992],{"class":86},[69,2672,2673],{"class":71,"line":225},[69,2674,2675],{"class":86},"  }\n",[69,2677,2678,2680],{"class":71,"line":240},[69,2679,1191],{"class":82},[69,2681,2615],{"class":86},[69,2683,2684,2687,2689,2691,2694,2696,2699,2701],{"class":71,"line":256},[69,2685,2686],{"class":109},"    find",[69,2688,1164],{"class":82},[69,2690,2601],{"class":82},[69,2692,2693],{"class":86},"(id) ",[69,2695,1100],{"class":82},[69,2697,2698],{"class":86}," users[id] ",[69,2700,1668],{"class":82},[69,2702,159],{"class":86},[69,2704,2705,2708,2710,2712,2715,2718,2720,2722,2725,2727,2729,2731],{"class":71,"line":280},[69,2706,2707],{"class":109},"    save",[69,2709,1164],{"class":82},[69,2711,2601],{"class":82},[69,2713,2714],{"class":86},"(user) users[user.",[69,2716,2717],{"class":109},"id",[69,2719,1513],{"class":86},[69,2721,90],{"class":82},[69,2723,2724],{"class":86}," user; ",[69,2726,1100],{"class":82},[69,2728,459],{"class":143},[69,2730,949],{"class":82},[69,2732,159],{"class":86},[69,2734,2735,2738,2740,2742,2745,2747,2749,2752,2754,2756,2758],{"class":71,"line":285},[69,2736,2737],{"class":109},"    delete",[69,2739,1164],{"class":82},[69,2741,2601],{"class":82},[69,2743,2744],{"class":86},"(id) users[id] ",[69,2746,90],{"class":82},[69,2748,319],{"class":143},[69,2750,2751],{"class":86},"; ",[69,2753,1100],{"class":82},[69,2755,459],{"class":143},[69,2757,949],{"class":82},[69,2759,159],{"class":86},[69,2761,2762],{"class":71,"line":290},[69,2763,2764],{"class":75},"    -- Track calls for verification\n",[69,2766,2767,2770,2772,2775,2777,2779,2782,2784,2786,2789,2791,2793],{"class":71,"line":295},[69,2768,2769],{"class":86},"    calls ",[69,2771,90],{"class":82},[69,2773,2774],{"class":86}," {find ",[69,2776,90],{"class":82},[69,2778,1217],{"class":143},[69,2780,2781],{"class":86},", save ",[69,2783,90],{"class":82},[69,2785,1217],{"class":143},[69,2787,2788],{"class":86},", delete ",[69,2790,90],{"class":82},[69,2792,1217],{"class":143},[69,2794,1992],{"class":86},[69,2796,2797],{"class":71,"line":310},[69,2798,2675],{"class":86},[69,2800,2801],{"class":71,"line":325},[69,2802,202],{"class":82},[69,2804,2805],{"class":71,"line":341},[69,2806,100],{"emptyLinePlaceholder":99},[69,2808,2809],{"class":71,"line":360},[69,2810,2811],{"class":75},"-- Set _ENV or modify the mock to track calls:\n",[69,2813,2814,2816],{"class":71,"line":365},[69,2815,83],{"class":82},[69,2817,2818],{"class":86}," mock_db\n",[69,2820,2821,2823,2825,2827,2829,2831,2833],{"class":71,"line":370},[69,2822,44],{"class":143},[69,2824,1496],{"class":86},[69,2826,2594],{"class":155},[69,2828,1513],{"class":86},[69,2830,90],{"class":82},[69,2832,2601],{"class":82},[69,2834,1702],{"class":86},[69,2836,2837,2840,2842],{"class":71,"line":375},[69,2838,2839],{"class":86},"  mock_db ",[69,2841,90],{"class":82},[69,2843,2615],{"class":86},[69,2845,2846,2849,2851,2854,2856,2858,2860,2862,2864,2866,2868,2870,2872],{"class":71,"line":389},[69,2847,2848],{"class":86},"    _users ",[69,2850,90],{"class":82},[69,2852,2853],{"class":86}," {[",[69,2855,2623],{"class":143},[69,2857,1513],{"class":86},[69,2859,90],{"class":82},[69,2861,2630],{"class":86},[69,2863,90],{"class":82},[69,2865,1510],{"class":143},[69,2867,2637],{"class":86},[69,2869,90],{"class":82},[69,2871,2642],{"class":155},[69,2873,2874],{"class":86},"}},\n",[69,2876,2877,2880,2882],{"class":71,"line":402},[69,2878,2879],{"class":86},"    _calls ",[69,2881,90],{"class":82},[69,2883,1232],{"class":86},[69,2885,2886,2888,2890,2892],{"class":71,"line":421},[69,2887,2686],{"class":109},[69,2889,1164],{"class":82},[69,2891,2601],{"class":82},[69,2893,2894],{"class":86},"(self, id)\n",[69,2896,2897,2900,2902,2905,2907,2910,2912,2915,2917,2919,2921,2923,2925,2927,2929,2931,2934],{"class":71,"line":426},[69,2898,2899],{"class":143},"      self",[69,2901,113],{"class":86},[69,2903,2904],{"class":109},"_calls",[69,2906,113],{"class":86},[69,2908,2909],{"class":109},"find",[69,2911,1164],{"class":82},[69,2913,2914],{"class":86}," (",[69,2916,1281],{"class":143},[69,2918,113],{"class":86},[69,2920,2904],{"class":109},[69,2922,113],{"class":86},[69,2924,2909],{"class":109},[69,2926,1621],{"class":82},[69,2928,1217],{"class":143},[69,2930,1665],{"class":86},[69,2932,2933],{"class":82},"+",[69,2935,1446],{"class":143},[69,2937,2938,2941,2943,2945,2948],{"class":71,"line":431},[69,2939,2940],{"class":82},"      return",[69,2942,1296],{"class":143},[69,2944,113],{"class":86},[69,2946,2947],{"class":109},"_users",[69,2949,2950],{"class":86},"[id]\n",[69,2952,2953,2956],{"class":71,"line":436},[69,2954,2955],{"class":82},"    end",[69,2957,159],{"class":86},[69,2959,2960,2962,2964,2966],{"class":71,"line":450},[69,2961,2707],{"class":109},[69,2963,1164],{"class":82},[69,2965,2601],{"class":82},[69,2967,2968],{"class":86},"(self, user)\n",[69,2970,2971,2973,2975,2977,2979,2982,2984,2986,2988,2990,2992,2994,2996,2998,3000,3002,3004],{"class":71,"line":468},[69,2972,2899],{"class":143},[69,2974,113],{"class":86},[69,2976,2904],{"class":109},[69,2978,113],{"class":86},[69,2980,2981],{"class":109},"save",[69,2983,1164],{"class":82},[69,2985,2914],{"class":86},[69,2987,1281],{"class":143},[69,2989,113],{"class":86},[69,2991,2904],{"class":109},[69,2993,113],{"class":86},[69,2995,2981],{"class":109},[69,2997,1621],{"class":82},[69,2999,1217],{"class":143},[69,3001,1665],{"class":86},[69,3003,2933],{"class":82},[69,3005,1446],{"class":143},[69,3007,3008,3010,3012,3014,3017,3019,3021,3023],{"class":71,"line":484},[69,3009,2899],{"class":143},[69,3011,113],{"class":86},[69,3013,2947],{"class":109},[69,3015,3016],{"class":86},"[user.",[69,3018,2717],{"class":109},[69,3020,1513],{"class":86},[69,3022,90],{"class":82},[69,3024,3025],{"class":86}," user\n",[69,3027,3028,3030],{"class":71,"line":509},[69,3029,2940],{"class":82},[69,3031,963],{"class":143},[69,3033,3034,3036],{"class":71,"line":514},[69,3035,2955],{"class":82},[69,3037,159],{"class":86},[69,3039,3040],{"class":71,"line":519},[69,3041,2675],{"class":86},[69,3043,3044],{"class":71,"line":524},[69,3045,3046],{"class":75},"  -- Return a table that auto-passes self (method syntax)\n",[69,3048,3049,3051,3053],{"class":71,"line":538},[69,3050,1191],{"class":82},[69,3052,1194],{"class":143},[69,3054,3055],{"class":86},"({}, {\n",[69,3057,3058,3061,3063,3065],{"class":71,"line":554},[69,3059,3060],{"class":109},"    __index",[69,3062,1164],{"class":82},[69,3064,2601],{"class":82},[69,3066,3067],{"class":86},"(_, k)\n",[69,3069,3070,3073,3075,3077,3080,3082,3084,3087,3089],{"class":71,"line":570},[69,3071,3072],{"class":82},"      if",[69,3074,1020],{"class":86},[69,3076,233],{"class":82},[69,3078,3079],{"class":155}," \"calls\" ",[69,3081,548],{"class":82},[69,3083,946],{"class":82},[69,3085,3086],{"class":86}," mock_db.",[69,3088,2904],{"class":109},[69,3090,1299],{"class":82},[69,3092,3093,3095,3097,3100,3102,3105,3108,3110],{"class":71,"line":589},[69,3094,2940],{"class":82},[69,3096,2601],{"class":82},[69,3098,3099],{"class":86},"(...) ",[69,3101,1100],{"class":82},[69,3103,3104],{"class":86}," mock_db[k](mock_db, ",[69,3106,3107],{"class":143},"...",[69,3109,1665],{"class":86},[69,3111,202],{"class":82},[69,3113,3114],{"class":71,"line":594},[69,3115,922],{"class":82},[69,3117,3118],{"class":71,"line":599},[69,3119,3120],{"class":86},"  })\n",[69,3122,3123],{"class":71,"line":604},[69,3124,202],{"class":82},[69,3126,3127],{"class":71,"line":619},[69,3128,100],{"emptyLinePlaceholder":99},[69,3130,3131],{"class":71,"line":636},[69,3132,3133],{"class":75},"-- NOW require the service — it gets the mock db\n",[69,3135,3136,3138,3141,3143,3145,3147,3150],{"class":71,"line":646},[69,3137,83],{"class":82},[69,3139,3140],{"class":86}," UserService ",[69,3142,90],{"class":82},[69,3144,1131],{"class":143},[69,3146,147],{"class":86},[69,3148,3149],{"class":155},"\"user_service\"",[69,3151,190],{"class":86},[69,3153,3154],{"class":71,"line":664},[69,3155,100],{"emptyLinePlaceholder":99},[69,3157,3158,3160,3162,3164,3166,3168,3170,3173,3175,3177,3180],{"class":71,"line":669},[69,3159,83],{"class":82},[69,3161,1924],{"class":86},[69,3163,90],{"class":82},[69,3165,1131],{"class":143},[69,3167,147],{"class":86},[69,3169,1872],{"class":155},[69,3171,3172],{"class":86},").",[69,3174,1183],{"class":143},[69,3176,147],{"class":86},[69,3178,3179],{"class":155},"\"UserService\"",[69,3181,190],{"class":86},[69,3183,3184],{"class":71,"line":696},[69,3185,100],{"emptyLinePlaceholder":99},[69,3187,3188,3190,3192,3194,3196,3199,3201,3203],{"class":71,"line":712},[69,3189,1951],{"class":109},[69,3191,1272],{"class":86},[69,3193,1344],{"class":143},[69,3195,147],{"class":86},[69,3197,3198],{"class":155},"\"get_user returns user from db\"",[69,3200,39],{"class":86},[69,3202,106],{"class":82},[69,3204,2215],{"class":86},[69,3206,3207,3209,3212,3214,3217,3220,3222,3224],{"class":71,"line":732},[69,3208,622],{"class":82},[69,3210,3211],{"class":86}," user ",[69,3213,90],{"class":82},[69,3215,3216],{"class":86}," UserService.",[69,3218,3219],{"class":143},"get_user",[69,3221,147],{"class":86},[69,3223,2623],{"class":143},[69,3225,190],{"class":86},[69,3227,3228,3230,3232,3235,3237,3239,3241,3243,3245],{"class":71,"line":737},[69,3229,2169],{"class":86},[69,3231,762],{"class":143},[69,3233,3234],{"class":86},"({id ",[69,3236,90],{"class":82},[69,3238,1510],{"class":143},[69,3240,2637],{"class":86},[69,3242,90],{"class":82},[69,3244,2642],{"class":155},[69,3246,3247],{"class":86},"}, user)\n",[69,3249,3250,3252],{"class":71,"line":742},[69,3251,1668],{"class":82},[69,3253,190],{"class":86},[69,3255,3256],{"class":71,"line":747},[69,3257,100],{"emptyLinePlaceholder":99},[69,3259,3260,3262,3264,3266,3268,3271,3273,3275],{"class":71,"line":753},[69,3261,1951],{"class":109},[69,3263,1272],{"class":86},[69,3265,1344],{"class":143},[69,3267,147],{"class":86},[69,3269,3270],{"class":155},"\"save_user calls db.save\"",[69,3272,39],{"class":86},[69,3274,106],{"class":82},[69,3276,2215],{"class":86},[69,3278,3279,3282,3285,3287,3289,3292,3294,3296,3299],{"class":71,"line":768},[69,3280,3281],{"class":86},"  UserService.",[69,3283,3284],{"class":143},"save_user",[69,3286,3234],{"class":86},[69,3288,90],{"class":82},[69,3290,3291],{"class":143}," 3",[69,3293,2637],{"class":86},[69,3295,90],{"class":82},[69,3297,3298],{"class":155}," \"Charlie\"",[69,3300,3301],{"class":86},"})\n",[69,3303,3304,3306,3308,3311,3313,3315,3317],{"class":71,"line":783},[69,3305,2169],{"class":86},[69,3307,384],{"class":143},[69,3309,3310],{"class":86},"(mock_db.",[69,3312,2904],{"class":109},[69,3314,113],{"class":86},[69,3316,2981],{"class":109},[69,3318,190],{"class":86},[69,3320,3321,3323,3325,3327,3329,3332,3334,3336,3338],{"class":71,"line":803},[69,3322,2169],{"class":86},[69,3324,116],{"class":143},[69,3326,147],{"class":86},[69,3328,2623],{"class":143},[69,3330,3331],{"class":86},", mock_db.",[69,3333,2904],{"class":109},[69,3335,113],{"class":86},[69,3337,2981],{"class":109},[69,3339,190],{"class":86},[69,3341,3342,3344],{"class":71,"line":819},[69,3343,1668],{"class":82},[69,3345,190],{"class":86},[69,3347,3348],{"class":71,"line":843},[69,3349,100],{"emptyLinePlaceholder":99},[69,3351,3352,3354,3356,3358],{"class":71,"line":848},[69,3353,1951],{"class":109},[69,3355,1272],{"class":86},[69,3357,1699],{"class":143},[69,3359,1702],{"class":86},[51,3361,3363],{"id":3362},"dependency-injection-for-testability","Dependency Injection for Testability",[56,3365,3366,3729,3733],{"language":58},[61,3367,3369],{"className":63,"code":3368,"language":58,"meta":65,"style":65},"-- Instead of hard require(), accept dependencies as parameters\n-- This makes modules testable without package.preload tricks\n\n-- BAD: hard dependency, untestable\nlocal http = require(\"luasocket.http\")\nlocal M = {}\nfunction M.fetch(url) return http.request(url) end\nreturn M\n\n-- GOOD: inject the http client\nlocal M = {}\nfunction M.new(deps)\n  local http = deps and deps.http or require(\"luasocket.http\")\n  return {\n    fetch = function(url) return http.request(url) end,\n  }\nend\nreturn M\n\n-- Test: pass a mock http\nlocal mock_http = {\n  request = function(url)\n    assert(url == \"https:\u002F\u002Fapi.example.com\u002Fdata\")\n    return \"mocked response\", 200, {}\n  end,\n}\nlocal client = require(\"myclient\").new({ http = mock_http })\nlocal body = client.fetch(\"https:\u002F\u002Fapi.example.com\u002Fdata\")\nassert(body == \"mocked response\")\n\n-- Production: use the real dependency\nlocal real_client = require(\"myclient\").new()  -- defaults to luasocket.http\n",[19,3370,3371,3376,3381,3385,3390,3408,3419,3446,3453,3457,3462,3472,3485,3514,3520,3543,3547,3551,3557,3561,3566,3577,3589,3604,3620,3627,3632,3660,3681,3695,3699,3704],{"__ignoreMap":65},[69,3372,3373],{"class":71,"line":72},[69,3374,3375],{"class":75},"-- Instead of hard require(), accept dependencies as parameters\n",[69,3377,3378],{"class":71,"line":79},[69,3379,3380],{"class":75},"-- This makes modules testable without package.preload tricks\n",[69,3382,3383],{"class":71,"line":96},[69,3384,100],{"emptyLinePlaceholder":99},[69,3386,3387],{"class":71,"line":103},[69,3388,3389],{"class":75},"-- BAD: hard dependency, untestable\n",[69,3391,3392,3394,3397,3399,3401,3403,3406],{"class":71,"line":122},[69,3393,83],{"class":82},[69,3395,3396],{"class":86}," http ",[69,3398,90],{"class":82},[69,3400,1131],{"class":143},[69,3402,147],{"class":86},[69,3404,3405],{"class":155},"\"luasocket.http\"",[69,3407,190],{"class":86},[69,3409,3410,3412,3415,3417],{"class":71,"line":140},[69,3411,83],{"class":82},[69,3413,3414],{"class":86}," M ",[69,3416,90],{"class":82},[69,3418,93],{"class":86},[69,3420,3421,3423,3426,3428,3431,3434,3436,3439,3442,3444],{"class":71,"line":162},[69,3422,106],{"class":82},[69,3424,3425],{"class":109}," M",[69,3427,113],{"class":86},[69,3429,3430],{"class":109},"fetch",[69,3432,3433],{"class":86},"(url) ",[69,3435,1100],{"class":82},[69,3437,3438],{"class":86}," http.",[69,3440,3441],{"class":143},"request",[69,3443,3433],{"class":86},[69,3445,202],{"class":82},[69,3447,3448,3450],{"class":71,"line":193},[69,3449,1100],{"class":82},[69,3451,3452],{"class":86}," M\n",[69,3454,3455],{"class":71,"line":199},[69,3456,100],{"emptyLinePlaceholder":99},[69,3458,3459],{"class":71,"line":205},[69,3460,3461],{"class":75},"-- GOOD: inject the http client\n",[69,3463,3464,3466,3468,3470],{"class":71,"line":210},[69,3465,83],{"class":82},[69,3467,3414],{"class":86},[69,3469,90],{"class":82},[69,3471,93],{"class":86},[69,3473,3474,3476,3478,3480,3482],{"class":71,"line":225},[69,3475,106],{"class":82},[69,3477,3425],{"class":109},[69,3479,113],{"class":86},[69,3481,1183],{"class":109},[69,3483,3484],{"class":86},"(deps)\n",[69,3486,3487,3489,3491,3493,3496,3498,3501,3504,3506,3508,3510,3512],{"class":71,"line":240},[69,3488,622],{"class":82},[69,3490,3396],{"class":86},[69,3492,90],{"class":82},[69,3494,3495],{"class":86}," deps ",[69,3497,677],{"class":82},[69,3499,3500],{"class":86}," deps.",[69,3502,3503],{"class":109},"http",[69,3505,1621],{"class":82},[69,3507,1131],{"class":143},[69,3509,147],{"class":86},[69,3511,3405],{"class":155},[69,3513,190],{"class":86},[69,3515,3516,3518],{"class":71,"line":256},[69,3517,1191],{"class":82},[69,3519,2615],{"class":86},[69,3521,3522,3525,3527,3529,3531,3533,3535,3537,3539,3541],{"class":71,"line":280},[69,3523,3524],{"class":109},"    fetch",[69,3526,1164],{"class":82},[69,3528,2601],{"class":82},[69,3530,3433],{"class":86},[69,3532,1100],{"class":82},[69,3534,3438],{"class":86},[69,3536,3441],{"class":143},[69,3538,3433],{"class":86},[69,3540,1668],{"class":82},[69,3542,159],{"class":86},[69,3544,3545],{"class":71,"line":285},[69,3546,2675],{"class":86},[69,3548,3549],{"class":71,"line":290},[69,3550,202],{"class":82},[69,3552,3553,3555],{"class":71,"line":295},[69,3554,1100],{"class":82},[69,3556,3452],{"class":86},[69,3558,3559],{"class":71,"line":310},[69,3560,100],{"emptyLinePlaceholder":99},[69,3562,3563],{"class":71,"line":325},[69,3564,3565],{"class":75},"-- Test: pass a mock http\n",[69,3567,3568,3570,3573,3575],{"class":71,"line":341},[69,3569,83],{"class":82},[69,3571,3572],{"class":86}," mock_http ",[69,3574,90],{"class":82},[69,3576,2615],{"class":86},[69,3578,3579,3582,3584,3586],{"class":71,"line":360},[69,3580,3581],{"class":109},"  request",[69,3583,1164],{"class":82},[69,3585,2601],{"class":82},[69,3587,3588],{"class":86},"(url)\n",[69,3590,3591,3594,3597,3599,3602],{"class":71,"line":365},[69,3592,3593],{"class":143},"    assert",[69,3595,3596],{"class":86},"(url ",[69,3598,233],{"class":82},[69,3600,3601],{"class":155}," \"https:\u002F\u002Fapi.example.com\u002Fdata\"",[69,3603,190],{"class":86},[69,3605,3606,3609,3612,3614,3617],{"class":71,"line":370},[69,3607,3608],{"class":82},"    return",[69,3610,3611],{"class":155}," \"mocked response\"",[69,3613,39],{"class":86},[69,3615,3616],{"class":143},"200",[69,3618,3619],{"class":86},", {}\n",[69,3621,3622,3625],{"class":71,"line":375},[69,3623,3624],{"class":82},"  end",[69,3626,159],{"class":86},[69,3628,3629],{"class":71,"line":389},[69,3630,3631],{"class":86},"}\n",[69,3633,3634,3636,3639,3641,3643,3645,3648,3650,3652,3655,3657],{"class":71,"line":402},[69,3635,83],{"class":82},[69,3637,3638],{"class":86}," client ",[69,3640,90],{"class":82},[69,3642,1131],{"class":143},[69,3644,147],{"class":86},[69,3646,3647],{"class":155},"\"myclient\"",[69,3649,3172],{"class":86},[69,3651,1183],{"class":143},[69,3653,3654],{"class":86},"({ http ",[69,3656,90],{"class":82},[69,3658,3659],{"class":86}," mock_http })\n",[69,3661,3662,3664,3667,3669,3672,3674,3676,3679],{"class":71,"line":421},[69,3663,83],{"class":82},[69,3665,3666],{"class":86}," body ",[69,3668,90],{"class":82},[69,3670,3671],{"class":86}," client.",[69,3673,3430],{"class":143},[69,3675,147],{"class":86},[69,3677,3678],{"class":155},"\"https:\u002F\u002Fapi.example.com\u002Fdata\"",[69,3680,190],{"class":86},[69,3682,3683,3686,3689,3691,3693],{"class":71,"line":426},[69,3684,3685],{"class":143},"assert",[69,3687,3688],{"class":86},"(body ",[69,3690,233],{"class":82},[69,3692,3611],{"class":155},[69,3694,190],{"class":86},[69,3696,3697],{"class":71,"line":431},[69,3698,100],{"emptyLinePlaceholder":99},[69,3700,3701],{"class":71,"line":436},[69,3702,3703],{"class":75},"-- Production: use the real dependency\n",[69,3705,3706,3708,3711,3713,3715,3717,3719,3721,3723,3726],{"class":71,"line":450},[69,3707,83],{"class":82},[69,3709,3710],{"class":86}," real_client ",[69,3712,90],{"class":82},[69,3714,1131],{"class":143},[69,3716,147],{"class":86},[69,3718,3647],{"class":155},[69,3720,3172],{"class":86},[69,3722,1183],{"class":143},[69,3724,3725],{"class":86},"()  ",[69,3727,3728],{"class":75},"-- defaults to luasocket.http\n",[51,3730,3732],{"id":3731},"testing-coroutines-async","Testing Coroutines & Async",[56,3734,3735,4104,4111],{"language":58},[61,3736,3738],{"className":63,"code":3737,"language":58,"meta":65,"style":65},"-- Test a coroutine-based generator by collecting its output\nlocal A = require(\"assert\")\n\nlocal function collect_generator(gen)\n  local results = {}\n  for v in gen do\n    results[#results + 1] = v\n  end\n  return results\nend\n\n-- Test: fibonacci generator\nlocal function fib_gen(n)\n  return coroutine.wrap(function()\n    local a, b = 0, 1\n    for _ = 1, n do\n      coroutine.yield(b)\n      a, b = b, a + b\n    end\n  end)\nend\n\nA.deep_equals({1, 1, 2, 3, 5, 8}, collect_generator(fib_gen(6)))\nA.deep_equals({1}, collect_generator(fib_gen(1)))\nA.deep_equals({}, collect_generator(fib_gen(0)))\n\n-- Test: coroutine error handling\nA.raises(function()\n  coroutine.wrap(function()\n    error(\"boom\")\n  end)()\nend, \"boom\")\n",[19,3739,3740,3745,3761,3765,3777,3788,3802,3823,3827,3834,3838,3842,3847,3859,3872,3888,3905,3913,3928,3932,3938,3942,3946,3998,4022,4044,4048,4053,4065,4076,4087,4094],{"__ignoreMap":65},[69,3741,3742],{"class":71,"line":72},[69,3743,3744],{"class":75},"-- Test a coroutine-based generator by collecting its output\n",[69,3746,3747,3749,3751,3753,3755,3757,3759],{"class":71,"line":79},[69,3748,83],{"class":82},[69,3750,1881],{"class":86},[69,3752,90],{"class":82},[69,3754,1131],{"class":143},[69,3756,147],{"class":86},[69,3758,1136],{"class":155},[69,3760,190],{"class":86},[69,3762,3763],{"class":71,"line":96},[69,3764,100],{"emptyLinePlaceholder":99},[69,3766,3767,3769,3771,3774],{"class":71,"line":103},[69,3768,83],{"class":82},[69,3770,2601],{"class":82},[69,3772,3773],{"class":109}," collect_generator",[69,3775,3776],{"class":86},"(gen)\n",[69,3778,3779,3781,3784,3786],{"class":71,"line":122},[69,3780,622],{"class":82},[69,3782,3783],{"class":86}," results ",[69,3785,90],{"class":82},[69,3787,93],{"class":86},[69,3789,3790,3792,3795,3797,3800],{"class":71,"line":140},[69,3791,969],{"class":82},[69,3793,3794],{"class":86}," v ",[69,3796,975],{"class":82},[69,3798,3799],{"class":86}," gen ",[69,3801,983],{"class":82},[69,3803,3804,3807,3809,3812,3814,3816,3818,3820],{"class":71,"line":162},[69,3805,3806],{"class":86},"    results[",[69,3808,1499],{"class":82},[69,3810,3811],{"class":86},"results ",[69,3813,2933],{"class":82},[69,3815,1510],{"class":143},[69,3817,1513],{"class":86},[69,3819,90],{"class":82},[69,3821,3822],{"class":86}," v\n",[69,3824,3825],{"class":71,"line":193},[69,3826,196],{"class":82},[69,3828,3829,3831],{"class":71,"line":199},[69,3830,1191],{"class":82},[69,3832,3833],{"class":86}," results\n",[69,3835,3836],{"class":71,"line":205},[69,3837,202],{"class":82},[69,3839,3840],{"class":71,"line":210},[69,3841,100],{"emptyLinePlaceholder":99},[69,3843,3844],{"class":71,"line":225},[69,3845,3846],{"class":75},"-- Test: fibonacci generator\n",[69,3848,3849,3851,3853,3856],{"class":71,"line":240},[69,3850,83],{"class":82},[69,3852,2601],{"class":82},[69,3854,3855],{"class":109}," fib_gen",[69,3857,3858],{"class":86},"(n)\n",[69,3860,3861,3863,3866,3868,3870],{"class":71,"line":256},[69,3862,1191],{"class":82},[69,3864,3865],{"class":143}," coroutine.wrap",[69,3867,147],{"class":86},[69,3869,106],{"class":82},[69,3871,1702],{"class":86},[69,3873,3874,3876,3879,3881,3883,3885],{"class":71,"line":280},[69,3875,1599],{"class":82},[69,3877,3878],{"class":86}," a, b ",[69,3880,90],{"class":82},[69,3882,1217],{"class":143},[69,3884,39],{"class":86},[69,3886,3887],{"class":143},"1\n",[69,3889,3890,3893,3896,3898,3900,3903],{"class":71,"line":285},[69,3891,3892],{"class":82},"    for",[69,3894,3895],{"class":86}," _ ",[69,3897,90],{"class":82},[69,3899,1510],{"class":143},[69,3901,3902],{"class":86},", n ",[69,3904,983],{"class":82},[69,3906,3907,3910],{"class":71,"line":290},[69,3908,3909],{"class":143},"      coroutine.yield",[69,3911,3912],{"class":86},"(b)\n",[69,3914,3915,3918,3920,3923,3925],{"class":71,"line":295},[69,3916,3917],{"class":86},"      a, b ",[69,3919,90],{"class":82},[69,3921,3922],{"class":86}," b, a ",[69,3924,2933],{"class":82},[69,3926,3927],{"class":86}," b\n",[69,3929,3930],{"class":71,"line":310},[69,3931,922],{"class":82},[69,3933,3934,3936],{"class":71,"line":325},[69,3935,3624],{"class":82},[69,3937,190],{"class":86},[69,3939,3940],{"class":71,"line":341},[69,3941,202],{"class":82},[69,3943,3944],{"class":71,"line":360},[69,3945,100],{"emptyLinePlaceholder":99},[69,3947,3948,3951,3953,3955,3957,3959,3961,3963,3965,3967,3970,3972,3975,3977,3980,3982,3985,3987,3990,3992,3995],{"class":71,"line":365},[69,3949,3950],{"class":86},"A.",[69,3952,762],{"class":143},[69,3954,2224],{"class":86},[69,3956,2623],{"class":143},[69,3958,39],{"class":86},[69,3960,2623],{"class":143},[69,3962,39],{"class":86},[69,3964,187],{"class":143},[69,3966,39],{"class":86},[69,3968,3969],{"class":143},"3",[69,3971,39],{"class":86},[69,3973,3974],{"class":143},"5",[69,3976,39],{"class":86},[69,3978,3979],{"class":143},"8",[69,3981,2159],{"class":86},[69,3983,3984],{"class":143},"collect_generator",[69,3986,147],{"class":86},[69,3988,3989],{"class":143},"fib_gen",[69,3991,147],{"class":86},[69,3993,3994],{"class":143},"6",[69,3996,3997],{"class":86},")))\n",[69,3999,4000,4002,4004,4006,4008,4010,4012,4014,4016,4018,4020],{"class":71,"line":370},[69,4001,3950],{"class":86},[69,4003,762],{"class":143},[69,4005,2224],{"class":86},[69,4007,2623],{"class":143},[69,4009,2159],{"class":86},[69,4011,3984],{"class":143},[69,4013,147],{"class":86},[69,4015,3989],{"class":143},[69,4017,147],{"class":86},[69,4019,2623],{"class":143},[69,4021,3997],{"class":86},[69,4023,4024,4026,4028,4031,4033,4035,4037,4039,4042],{"class":71,"line":375},[69,4025,3950],{"class":86},[69,4027,762],{"class":143},[69,4029,4030],{"class":86},"({}, ",[69,4032,3984],{"class":143},[69,4034,147],{"class":86},[69,4036,3989],{"class":143},[69,4038,147],{"class":86},[69,4040,4041],{"class":143},"0",[69,4043,3997],{"class":86},[69,4045,4046],{"class":71,"line":389},[69,4047,100],{"emptyLinePlaceholder":99},[69,4049,4050],{"class":71,"line":402},[69,4051,4052],{"class":75},"-- Test: coroutine error handling\n",[69,4054,4055,4057,4059,4061,4063],{"class":71,"line":421},[69,4056,3950],{"class":86},[69,4058,613],{"class":143},[69,4060,147],{"class":86},[69,4062,106],{"class":82},[69,4064,1702],{"class":86},[69,4066,4067,4070,4072,4074],{"class":71,"line":426},[69,4068,4069],{"class":143},"  coroutine.wrap",[69,4071,147],{"class":86},[69,4073,106],{"class":82},[69,4075,1702],{"class":86},[69,4077,4078,4080,4082,4085],{"class":71,"line":431},[69,4079,144],{"class":143},[69,4081,147],{"class":86},[69,4083,4084],{"class":155},"\"boom\"",[69,4086,190],{"class":86},[69,4088,4089,4091],{"class":71,"line":436},[69,4090,3624],{"class":82},[69,4092,4093],{"class":86},")()\n",[69,4095,4096,4098,4100,4102],{"class":71,"line":450},[69,4097,1668],{"class":82},[69,4099,39],{"class":86},[69,4101,4084],{"class":155},[69,4103,190],{"class":86},[51,4105,4107,4108],{"id":4106},"coverage-measurement-with-debugsethook","Coverage Measurement with ",[19,4109,4110],{},"debug.sethook",[56,4112,4114,4809,4816],{"language":58,"filename":4113},"coverage.lua",[61,4115,4117],{"className":63,"code":4116,"language":58,"meta":65,"style":65},"-- Simple line coverage: which lines of a module were executed\nlocal Coverage = {}\nCoverage.__index = Coverage\n\nfunction Coverage.new(source_file)\n  return setmetatable({\n    file = source_file,\n    hit = {},\n    total_lines = 0,\n  }, Coverage)\nend\n\nfunction Coverage:start()\n  self.hit = {}\n  local file = self.file\n  debug.sethook(function(event, line)\n    local info = debug.getinfo(2, \"S\")\n    if info.source:match(file) then\n      self.hit[line] = (self.hit[line] or 0) + 1\n    end\n  end, \"l\")\nend\n\nfunction Coverage:stop()\n  debug.sethook()\nend\n\nfunction Coverage:report()\n  -- Count total executable lines (simple heuristic: non-blank, non-comment)\n  local f = io.open(self.file, \"r\")\n  if not f then return nil, \"cannot open\" end\n  local total = 0\n  local covered = 0\n  local line_num = 0\n  for line in f:lines() do\n    line_num = line_num + 1\n    local stripped = line:gsub(\"^%s+\", \"\")\n    if stripped ~= \"\" and not stripped:match(\"^%-%-\") then\n      total = total + 1\n      if self.hit[line_num] then\n        covered = covered + 1\n      end\n    end\n  end\n  f:close()\n  local pct = total > 0 and (covered \u002F total * 100) or 0\n  return {\n    file = self.file,\n    total = total,\n    covered = covered,\n    missed = total - covered,\n    percent = pct,\n  }\nend\n\n-- Usage:\n-- local cov = Coverage.new(\"src\u002Fmy_module.lua\")\n-- cov:start()\n-- run_tests()\n-- cov:stop()\n-- local report = cov:report()\n-- print(string.format(\"%s: %d\u002F%d lines (%.1f%%)\",\n--   report.file, report.covered, report.total, report.percent))\n",[19,4118,4119,4124,4135,4147,4151,4165,4173,4183,4192,4203,4208,4212,4216,4229,4243,4259,4271,4294,4313,4346,4350,4361,4365,4369,4382,4388,4392,4396,4409,4414,4442,4463,4474,4485,4496,4517,4530,4558,4589,4602,4617,4630,4635,4639,4643,4655,4693,4699,4713,4723,4733,4747,4757,4761,4765,4769,4774,4779,4784,4789,4794,4799,4804],{"__ignoreMap":65},[69,4120,4121],{"class":71,"line":72},[69,4122,4123],{"class":75},"-- Simple line coverage: which lines of a module were executed\n",[69,4125,4126,4128,4131,4133],{"class":71,"line":79},[69,4127,83],{"class":82},[69,4129,4130],{"class":86}," Coverage ",[69,4132,90],{"class":82},[69,4134,93],{"class":86},[69,4136,4137,4140,4142,4144],{"class":71,"line":96},[69,4138,4139],{"class":86},"Coverage.",[69,4141,1161],{"class":109},[69,4143,1164],{"class":82},[69,4145,4146],{"class":86}," Coverage\n",[69,4148,4149],{"class":71,"line":103},[69,4150,100],{"emptyLinePlaceholder":99},[69,4152,4153,4155,4158,4160,4162],{"class":71,"line":122},[69,4154,106],{"class":82},[69,4156,4157],{"class":109}," Coverage",[69,4159,113],{"class":86},[69,4161,1183],{"class":109},[69,4163,4164],{"class":86},"(source_file)\n",[69,4166,4167,4169,4171],{"class":71,"line":140},[69,4168,1191],{"class":82},[69,4170,1194],{"class":143},[69,4172,1197],{"class":86},[69,4174,4175,4178,4180],{"class":71,"line":162},[69,4176,4177],{"class":86},"    file ",[69,4179,90],{"class":82},[69,4181,4182],{"class":86}," source_file,\n",[69,4184,4185,4188,4190],{"class":71,"line":193},[69,4186,4187],{"class":86},"    hit ",[69,4189,90],{"class":82},[69,4191,1232],{"class":86},[69,4193,4194,4197,4199,4201],{"class":71,"line":199},[69,4195,4196],{"class":86},"    total_lines ",[69,4198,90],{"class":82},[69,4200,1217],{"class":143},[69,4202,159],{"class":86},[69,4204,4205],{"class":71,"line":205},[69,4206,4207],{"class":86},"  }, Coverage)\n",[69,4209,4210],{"class":71,"line":210},[69,4211,202],{"class":82},[69,4213,4214],{"class":71,"line":225},[69,4215,100],{"emptyLinePlaceholder":99},[69,4217,4218,4220,4222,4224,4227],{"class":71,"line":240},[69,4219,106],{"class":82},[69,4221,4157],{"class":109},[69,4223,1272],{"class":86},[69,4225,4226],{"class":109},"start",[69,4228,1702],{"class":86},[69,4230,4231,4234,4236,4239,4241],{"class":71,"line":256},[69,4232,4233],{"class":143},"  self",[69,4235,113],{"class":86},[69,4237,4238],{"class":109},"hit",[69,4240,1164],{"class":82},[69,4242,93],{"class":86},[69,4244,4245,4247,4250,4252,4254,4256],{"class":71,"line":280},[69,4246,622],{"class":82},[69,4248,4249],{"class":86}," file ",[69,4251,90],{"class":82},[69,4253,1296],{"class":143},[69,4255,113],{"class":86},[69,4257,4258],{"class":109},"file\n",[69,4260,4261,4264,4266,4268],{"class":71,"line":285},[69,4262,4263],{"class":143},"  debug.sethook",[69,4265,147],{"class":86},[69,4267,106],{"class":82},[69,4269,4270],{"class":86},"(event, line)\n",[69,4272,4273,4275,4278,4280,4283,4285,4287,4289,4292],{"class":71,"line":290},[69,4274,1599],{"class":82},[69,4276,4277],{"class":86}," info ",[69,4279,90],{"class":82},[69,4281,4282],{"class":143}," debug.getinfo",[69,4284,147],{"class":86},[69,4286,187],{"class":143},[69,4288,39],{"class":86},[69,4290,4291],{"class":155},"\"S\"",[69,4293,190],{"class":86},[69,4295,4296,4298,4301,4304,4306,4308,4311],{"class":71,"line":295},[69,4297,867],{"class":82},[69,4299,4300],{"class":86}," info.",[69,4302,4303],{"class":109},"source",[69,4305,1272],{"class":86},[69,4307,688],{"class":143},[69,4309,4310],{"class":86},"(file) ",[69,4312,137],{"class":82},[69,4314,4315,4317,4319,4321,4324,4326,4328,4330,4332,4334,4336,4338,4340,4342,4344],{"class":71,"line":310},[69,4316,2899],{"class":143},[69,4318,113],{"class":86},[69,4320,4238],{"class":109},[69,4322,4323],{"class":86},"[line] ",[69,4325,90],{"class":82},[69,4327,2914],{"class":86},[69,4329,1281],{"class":143},[69,4331,113],{"class":86},[69,4333,4238],{"class":109},[69,4335,4323],{"class":86},[69,4337,168],{"class":82},[69,4339,1217],{"class":143},[69,4341,1665],{"class":86},[69,4343,2933],{"class":82},[69,4345,1446],{"class":143},[69,4347,4348],{"class":71,"line":325},[69,4349,922],{"class":82},[69,4351,4352,4354,4356,4359],{"class":71,"line":341},[69,4353,3624],{"class":82},[69,4355,39],{"class":86},[69,4357,4358],{"class":155},"\"l\"",[69,4360,190],{"class":86},[69,4362,4363],{"class":71,"line":360},[69,4364,202],{"class":82},[69,4366,4367],{"class":71,"line":365},[69,4368,100],{"emptyLinePlaceholder":99},[69,4370,4371,4373,4375,4377,4380],{"class":71,"line":370},[69,4372,106],{"class":82},[69,4374,4157],{"class":109},[69,4376,1272],{"class":86},[69,4378,4379],{"class":109},"stop",[69,4381,1702],{"class":86},[69,4383,4384,4386],{"class":71,"line":375},[69,4385,4263],{"class":143},[69,4387,1702],{"class":86},[69,4389,4390],{"class":71,"line":389},[69,4391,202],{"class":82},[69,4393,4394],{"class":71,"line":402},[69,4395,100],{"emptyLinePlaceholder":99},[69,4397,4398,4400,4402,4404,4407],{"class":71,"line":421},[69,4399,106],{"class":82},[69,4401,4157],{"class":109},[69,4403,1272],{"class":86},[69,4405,4406],{"class":109},"report",[69,4408,1702],{"class":86},[69,4410,4411],{"class":71,"line":426},[69,4412,4413],{"class":75},"  -- Count total executable lines (simple heuristic: non-blank, non-comment)\n",[69,4415,4416,4418,4421,4423,4426,4428,4430,4432,4435,4437,4440],{"class":71,"line":431},[69,4417,622],{"class":82},[69,4419,4420],{"class":86}," f ",[69,4422,90],{"class":82},[69,4424,4425],{"class":143}," io.open",[69,4427,147],{"class":86},[69,4429,1281],{"class":143},[69,4431,113],{"class":86},[69,4433,4434],{"class":109},"file",[69,4436,39],{"class":86},[69,4438,4439],{"class":155},"\"r\"",[69,4441,190],{"class":86},[69,4443,4444,4446,4448,4450,4452,4454,4456,4458,4461],{"class":71,"line":436},[69,4445,125],{"class":82},[69,4447,543],{"class":82},[69,4449,4420],{"class":86},[69,4451,548],{"class":82},[69,4453,946],{"class":82},[69,4455,319],{"class":143},[69,4457,39],{"class":86},[69,4459,4460],{"class":155},"\"cannot open\" ",[69,4462,202],{"class":82},[69,4464,4465,4467,4470,4472],{"class":71,"line":450},[69,4466,622],{"class":82},[69,4468,4469],{"class":86}," total ",[69,4471,90],{"class":82},[69,4473,1830],{"class":143},[69,4475,4476,4478,4481,4483],{"class":71,"line":468},[69,4477,622],{"class":82},[69,4479,4480],{"class":86}," covered ",[69,4482,90],{"class":82},[69,4484,1830],{"class":143},[69,4486,4487,4489,4492,4494],{"class":71,"line":484},[69,4488,622],{"class":82},[69,4490,4491],{"class":86}," line_num ",[69,4493,90],{"class":82},[69,4495,1830],{"class":143},[69,4497,4498,4500,4503,4505,4508,4510,4513,4515],{"class":71,"line":509},[69,4499,969],{"class":82},[69,4501,4502],{"class":86}," line ",[69,4504,975],{"class":82},[69,4506,4507],{"class":109}," f",[69,4509,1272],{"class":86},[69,4511,4512],{"class":143},"lines",[69,4514,1374],{"class":86},[69,4516,983],{"class":82},[69,4518,4519,4522,4524,4526,4528],{"class":71,"line":514},[69,4520,4521],{"class":86},"    line_num ",[69,4523,90],{"class":82},[69,4525,4491],{"class":86},[69,4527,2933],{"class":82},[69,4529,1446],{"class":143},[69,4531,4532,4534,4537,4539,4542,4544,4547,4549,4552,4554,4556],{"class":71,"line":519},[69,4533,1599],{"class":82},[69,4535,4536],{"class":86}," stripped ",[69,4538,90],{"class":82},[69,4540,4541],{"class":109}," line",[69,4543,1272],{"class":86},[69,4545,4546],{"class":143},"gsub",[69,4548,147],{"class":86},[69,4550,4551],{"class":155},"\"^%s+\"",[69,4553,39],{"class":86},[69,4555,2227],{"class":155},[69,4557,190],{"class":86},[69,4559,4560,4562,4564,4566,4569,4571,4573,4576,4578,4580,4582,4585,4587],{"class":71,"line":524},[69,4561,867],{"class":82},[69,4563,4536],{"class":86},[69,4565,131],{"class":82},[69,4567,4568],{"class":155}," \"\" ",[69,4570,677],{"class":82},[69,4572,543],{"class":82},[69,4574,4575],{"class":109}," stripped",[69,4577,1272],{"class":86},[69,4579,688],{"class":143},[69,4581,147],{"class":86},[69,4583,4584],{"class":155},"\"^%-%-\"",[69,4586,1665],{"class":86},[69,4588,137],{"class":82},[69,4590,4591,4594,4596,4598,4600],{"class":71,"line":538},[69,4592,4593],{"class":86},"      total ",[69,4595,90],{"class":82},[69,4597,4469],{"class":86},[69,4599,2933],{"class":82},[69,4601,1446],{"class":143},[69,4603,4604,4606,4608,4610,4612,4615],{"class":71,"line":554},[69,4605,3072],{"class":82},[69,4607,1296],{"class":143},[69,4609,113],{"class":86},[69,4611,4238],{"class":109},[69,4613,4614],{"class":86},"[line_num] ",[69,4616,137],{"class":82},[69,4618,4619,4622,4624,4626,4628],{"class":71,"line":570},[69,4620,4621],{"class":86},"        covered ",[69,4623,90],{"class":82},[69,4625,4480],{"class":86},[69,4627,2933],{"class":82},[69,4629,1446],{"class":143},[69,4631,4632],{"class":71,"line":589},[69,4633,4634],{"class":82},"      end\n",[69,4636,4637],{"class":71,"line":594},[69,4638,922],{"class":82},[69,4640,4641],{"class":71,"line":599},[69,4642,196],{"class":82},[69,4644,4645,4648,4650,4653],{"class":71,"line":604},[69,4646,4647],{"class":109},"  f",[69,4649,1272],{"class":86},[69,4651,4652],{"class":143},"close",[69,4654,1702],{"class":86},[69,4656,4657,4659,4662,4664,4666,4669,4671,4673,4676,4679,4681,4684,4687,4689,4691],{"class":71,"line":619},[69,4658,622],{"class":82},[69,4660,4661],{"class":86}," pct ",[69,4663,90],{"class":82},[69,4665,4469],{"class":86},[69,4667,4668],{"class":82},">",[69,4670,1217],{"class":143},[69,4672,1365],{"class":82},[69,4674,4675],{"class":86}," (covered ",[69,4677,4678],{"class":82},"\u002F",[69,4680,4469],{"class":86},[69,4682,4683],{"class":82},"*",[69,4685,4686],{"class":143}," 100",[69,4688,1665],{"class":86},[69,4690,168],{"class":82},[69,4692,1830],{"class":143},[69,4694,4695,4697],{"class":71,"line":636},[69,4696,1191],{"class":82},[69,4698,2615],{"class":86},[69,4700,4701,4703,4705,4707,4709,4711],{"class":71,"line":646},[69,4702,4177],{"class":86},[69,4704,90],{"class":82},[69,4706,1296],{"class":143},[69,4708,113],{"class":86},[69,4710,4434],{"class":109},[69,4712,159],{"class":86},[69,4714,4715,4718,4720],{"class":71,"line":664},[69,4716,4717],{"class":86},"    total ",[69,4719,90],{"class":82},[69,4721,4722],{"class":86}," total,\n",[69,4724,4725,4728,4730],{"class":71,"line":669},[69,4726,4727],{"class":86},"    covered ",[69,4729,90],{"class":82},[69,4731,4732],{"class":86}," covered,\n",[69,4734,4735,4738,4740,4742,4745],{"class":71,"line":696},[69,4736,4737],{"class":86},"    missed ",[69,4739,90],{"class":82},[69,4741,4469],{"class":86},[69,4743,4744],{"class":82},"-",[69,4746,4732],{"class":86},[69,4748,4749,4752,4754],{"class":71,"line":712},[69,4750,4751],{"class":86},"    percent ",[69,4753,90],{"class":82},[69,4755,4756],{"class":86}," pct,\n",[69,4758,4759],{"class":71,"line":732},[69,4760,2675],{"class":86},[69,4762,4763],{"class":71,"line":737},[69,4764,202],{"class":82},[69,4766,4767],{"class":71,"line":742},[69,4768,100],{"emptyLinePlaceholder":99},[69,4770,4771],{"class":71,"line":747},[69,4772,4773],{"class":75},"-- Usage:\n",[69,4775,4776],{"class":71,"line":753},[69,4777,4778],{"class":75},"-- local cov = Coverage.new(\"src\u002Fmy_module.lua\")\n",[69,4780,4781],{"class":71,"line":768},[69,4782,4783],{"class":75},"-- cov:start()\n",[69,4785,4786],{"class":71,"line":783},[69,4787,4788],{"class":75},"-- run_tests()\n",[69,4790,4791],{"class":71,"line":803},[69,4792,4793],{"class":75},"-- cov:stop()\n",[69,4795,4796],{"class":71,"line":819},[69,4797,4798],{"class":75},"-- local report = cov:report()\n",[69,4800,4801],{"class":71,"line":843},[69,4802,4803],{"class":75},"-- print(string.format(\"%s: %d\u002F%d lines (%.1f%%)\",\n",[69,4805,4806],{"class":71,"line":848},[69,4807,4808],{"class":75},"--   report.file, report.covered, report.total, report.percent))\n",[51,4810,4812,4813],{"id":4811},"integration-testing-with-iopopen","Integration Testing with ",[19,4814,4815],{},"io.popen",[56,4817,4818,5091,5098],{"language":58},[61,4819,4821],{"className":63,"code":4820,"language":58,"meta":65,"style":65},"-- Integration test: run a script as a subprocess and check output\nlocal function run_script(script_path, args)\n  local cmd = string.format(\"lua %s %s\", script_path, args or \"\")\n  local pipe = assert(io.popen(cmd, \"r\"))\n  local output = pipe:read(\"*a\")\n  local ok, reason, code = pipe:close()\n  return output, code or 0\nend\n\nlocal A = require(\"assert\")\n\n-- Test: CLI tool outputs correct format\nlocal output, code = run_script(\"cli.lua\", \"--format=json input.txt\")\nA.equals(0, code, \"should exit 0\")\nA.is_truthy(output:match('\"name\":'), \"should output JSON\")\n\n-- Test: error handling for missing args\nlocal output2, code2 = run_script(\"cli.lua\", \"\")\nA.is_not_nil(code2 ~= 0, \"should exit non-zero on missing args\")\n",[19,4822,4823,4828,4840,4865,4888,4912,4929,4940,4944,4948,4964,4968,4973,4995,5013,5041,5045,5050,5071],{"__ignoreMap":65},[69,4824,4825],{"class":71,"line":72},[69,4826,4827],{"class":75},"-- Integration test: run a script as a subprocess and check output\n",[69,4829,4830,4832,4834,4837],{"class":71,"line":79},[69,4831,83],{"class":82},[69,4833,2601],{"class":82},[69,4835,4836],{"class":109}," run_script",[69,4838,4839],{"class":86},"(script_path, args)\n",[69,4841,4842,4844,4847,4849,4851,4853,4856,4859,4861,4863],{"class":71,"line":96},[69,4843,622],{"class":82},[69,4845,4846],{"class":86}," cmd ",[69,4848,90],{"class":82},[69,4850,1607],{"class":143},[69,4852,147],{"class":86},[69,4854,4855],{"class":155},"\"lua %s %s\"",[69,4857,4858],{"class":86},", script_path, args ",[69,4860,168],{"class":82},[69,4862,826],{"class":155},[69,4864,190],{"class":86},[69,4866,4867,4869,4872,4874,4877,4879,4881,4884,4886],{"class":71,"line":103},[69,4868,622],{"class":82},[69,4870,4871],{"class":86}," pipe ",[69,4873,90],{"class":82},[69,4875,4876],{"class":143}," assert",[69,4878,147],{"class":86},[69,4880,4815],{"class":143},[69,4882,4883],{"class":86},"(cmd, ",[69,4885,4439],{"class":155},[69,4887,1632],{"class":86},[69,4889,4890,4892,4895,4897,4900,4902,4905,4907,4910],{"class":71,"line":122},[69,4891,622],{"class":82},[69,4893,4894],{"class":86}," output ",[69,4896,90],{"class":82},[69,4898,4899],{"class":109}," pipe",[69,4901,1272],{"class":86},[69,4903,4904],{"class":143},"read",[69,4906,147],{"class":86},[69,4908,4909],{"class":155},"\"*a\"",[69,4911,190],{"class":86},[69,4913,4914,4916,4919,4921,4923,4925,4927],{"class":71,"line":140},[69,4915,622],{"class":82},[69,4917,4918],{"class":86}," ok, reason, code ",[69,4920,90],{"class":82},[69,4922,4899],{"class":109},[69,4924,1272],{"class":86},[69,4926,4652],{"class":143},[69,4928,1702],{"class":86},[69,4930,4931,4933,4936,4938],{"class":71,"line":162},[69,4932,1191],{"class":82},[69,4934,4935],{"class":86}," output, code ",[69,4937,168],{"class":82},[69,4939,1830],{"class":143},[69,4941,4942],{"class":71,"line":193},[69,4943,202],{"class":82},[69,4945,4946],{"class":71,"line":199},[69,4947,100],{"emptyLinePlaceholder":99},[69,4949,4950,4952,4954,4956,4958,4960,4962],{"class":71,"line":205},[69,4951,83],{"class":82},[69,4953,1881],{"class":86},[69,4955,90],{"class":82},[69,4957,1131],{"class":143},[69,4959,147],{"class":86},[69,4961,1136],{"class":155},[69,4963,190],{"class":86},[69,4965,4966],{"class":71,"line":210},[69,4967,100],{"emptyLinePlaceholder":99},[69,4969,4970],{"class":71,"line":225},[69,4971,4972],{"class":75},"-- Test: CLI tool outputs correct format\n",[69,4974,4975,4977,4979,4981,4983,4985,4988,4990,4993],{"class":71,"line":240},[69,4976,83],{"class":82},[69,4978,4935],{"class":86},[69,4980,90],{"class":82},[69,4982,4836],{"class":143},[69,4984,147],{"class":86},[69,4986,4987],{"class":155},"\"cli.lua\"",[69,4989,39],{"class":86},[69,4991,4992],{"class":155},"\"--format=json input.txt\"",[69,4994,190],{"class":86},[69,4996,4997,4999,5001,5003,5005,5008,5011],{"class":71,"line":256},[69,4998,3950],{"class":86},[69,5000,116],{"class":143},[69,5002,147],{"class":86},[69,5004,4041],{"class":143},[69,5006,5007],{"class":86},", code, ",[69,5009,5010],{"class":155},"\"should exit 0\"",[69,5012,190],{"class":86},[69,5014,5015,5017,5019,5021,5024,5026,5028,5030,5033,5036,5039],{"class":71,"line":280},[69,5016,3950],{"class":86},[69,5018,533],{"class":143},[69,5020,147],{"class":86},[69,5022,5023],{"class":109},"output",[69,5025,1272],{"class":86},[69,5027,688],{"class":143},[69,5029,147],{"class":86},[69,5031,5032],{"class":155},"'\"name\":'",[69,5034,5035],{"class":86},"), ",[69,5037,5038],{"class":155},"\"should output JSON\"",[69,5040,190],{"class":86},[69,5042,5043],{"class":71,"line":285},[69,5044,100],{"emptyLinePlaceholder":99},[69,5046,5047],{"class":71,"line":290},[69,5048,5049],{"class":75},"-- Test: error handling for missing args\n",[69,5051,5052,5054,5057,5059,5061,5063,5065,5067,5069],{"class":71,"line":295},[69,5053,83],{"class":82},[69,5055,5056],{"class":86}," output2, code2 ",[69,5058,90],{"class":82},[69,5060,4836],{"class":143},[69,5062,147],{"class":86},[69,5064,4987],{"class":155},[69,5066,39],{"class":86},[69,5068,2227],{"class":155},[69,5070,190],{"class":86},[69,5072,5073,5075,5077,5080,5082,5084,5086,5089],{"class":71,"line":310},[69,5074,3950],{"class":86},[69,5076,384],{"class":143},[69,5078,5079],{"class":86},"(code2 ",[69,5081,131],{"class":82},[69,5083,1217],{"class":143},[69,5085,39],{"class":86},[69,5087,5088],{"class":155},"\"should exit non-zero on missing args\"",[69,5090,190],{"class":86},[51,5092,5094,5095,5097],{"id":5093},"using-busted-bdd-style-framework","Using ",[19,5096,25],{}," (BDD-style framework)",[56,5099,5101,5138],{"language":5100},"bash",[61,5102,5105],{"className":5103,"code":5104,"language":5100,"meta":65,"style":65},"language-bash shiki shiki-themes github-light github-dark","luarocks install busted\nbusted spec\u002F            # run all tests in spec\u002F directory\nbusted --coverage       # with coverage\n",[19,5106,5107,5118,5128],{"__ignoreMap":65},[69,5108,5109,5112,5115],{"class":71,"line":72},[69,5110,5111],{"class":109},"luarocks",[69,5113,5114],{"class":155}," install",[69,5116,5117],{"class":155}," busted\n",[69,5119,5120,5122,5125],{"class":71,"line":79},[69,5121,25],{"class":109},[69,5123,5124],{"class":155}," spec\u002F",[69,5126,5127],{"class":75},"            # run all tests in spec\u002F directory\n",[69,5129,5130,5132,5135],{"class":71,"line":96},[69,5131,25],{"class":109},[69,5133,5134],{"class":143}," --coverage",[69,5136,5137],{"class":75},"       # with coverage\n",[56,5139,5141],{"language":58,"filename":5140},"spec\u002Fstring_utils_spec.lua",[61,5142,5144],{"className":63,"code":5143,"language":58,"meta":65,"style":65},"-- busted: describe\u002Fit blocks with before_each\u002Fafter_each\nlocal su = require(\"string_utils\")\n\ndescribe(\"string_utils.trim\", function()\n  it(\"removes leading whitespace\", function()\n    assert.are.equal(\"hello\", su.trim(\"  hello\"))\n  end)\n\n  it(\"removes trailing whitespace\", function()\n    assert.are.equal(\"hello\", su.trim(\"hello  \"))\n  end)\n\n  it(\"handles empty string\", function()\n    assert.are.equal(\"\", su.trim(\"\"))\n  end)\n\n  it(\"handles all-whitespace string\", function()\n    assert.are.equal(\"\", su.trim(\"   \"))\n  end)\nend)\n\ndescribe(\"string_utils.split\", function()\n  it(\"splits on delimiter\", function()\n    assert.are.same({\"a\", \"b\", \"c\"}, su.split(\"a,b,c\", \",\"))\n  end)\n\n  it(\"returns single element for no delimiter\", function()\n    assert.are.same({\"hello\"}, su.split(\"hello\", \",\"))\n  end)\n\n  it(\"handles trailing delimiter\", function()\n    assert.are.same({\"a\", \"b\", \"c\", \"\"}, su.split(\"a,b,c,\", \",\"))\n  end)\nend)\n",[19,5145,5146,5151,5167,5171,5187,5203,5233,5239,5243,5258,5285,5291,5295,5310,5336,5342,5346,5361,5388,5394,5400,5404,5419,5434,5473,5479,5483,5498,5528,5534,5538,5553,5595,5601],{"__ignoreMap":65},[69,5147,5148],{"class":71,"line":72},[69,5149,5150],{"class":75},"-- busted: describe\u002Fit blocks with before_each\u002Fafter_each\n",[69,5152,5153,5155,5157,5159,5161,5163,5165],{"class":71,"line":79},[69,5154,83],{"class":82},[69,5156,1898],{"class":86},[69,5158,90],{"class":82},[69,5160,1131],{"class":143},[69,5162,147],{"class":86},[69,5164,1907],{"class":155},[69,5166,190],{"class":86},[69,5168,5169],{"class":71,"line":96},[69,5170,100],{"emptyLinePlaceholder":99},[69,5172,5173,5176,5178,5181,5183,5185],{"class":71,"line":103},[69,5174,5175],{"class":143},"describe",[69,5177,147],{"class":86},[69,5179,5180],{"class":155},"\"string_utils.trim\"",[69,5182,39],{"class":86},[69,5184,106],{"class":82},[69,5186,1702],{"class":86},[69,5188,5189,5192,5194,5197,5199,5201],{"class":71,"line":122},[69,5190,5191],{"class":143},"  it",[69,5193,147],{"class":86},[69,5195,5196],{"class":155},"\"removes leading whitespace\"",[69,5198,39],{"class":86},[69,5200,106],{"class":82},[69,5202,1702],{"class":86},[69,5204,5205,5207,5209,5212,5214,5217,5219,5221,5224,5226,5228,5231],{"class":71,"line":140},[69,5206,3593],{"class":143},[69,5208,113],{"class":86},[69,5210,5211],{"class":109},"are",[69,5213,113],{"class":86},[69,5215,5216],{"class":143},"equal",[69,5218,147],{"class":86},[69,5220,2415],{"class":155},[69,5222,5223],{"class":86},", su.",[69,5225,2177],{"class":143},[69,5227,147],{"class":86},[69,5229,5230],{"class":155},"\"  hello\"",[69,5232,1632],{"class":86},[69,5234,5235,5237],{"class":71,"line":162},[69,5236,3624],{"class":82},[69,5238,190],{"class":86},[69,5240,5241],{"class":71,"line":193},[69,5242,100],{"emptyLinePlaceholder":99},[69,5244,5245,5247,5249,5252,5254,5256],{"class":71,"line":199},[69,5246,5191],{"class":143},[69,5248,147],{"class":86},[69,5250,5251],{"class":155},"\"removes trailing whitespace\"",[69,5253,39],{"class":86},[69,5255,106],{"class":82},[69,5257,1702],{"class":86},[69,5259,5260,5262,5264,5266,5268,5270,5272,5274,5276,5278,5280,5283],{"class":71,"line":205},[69,5261,3593],{"class":143},[69,5263,113],{"class":86},[69,5265,5211],{"class":109},[69,5267,113],{"class":86},[69,5269,5216],{"class":143},[69,5271,147],{"class":86},[69,5273,2415],{"class":155},[69,5275,5223],{"class":86},[69,5277,2177],{"class":143},[69,5279,147],{"class":86},[69,5281,5282],{"class":155},"\"hello  \"",[69,5284,1632],{"class":86},[69,5286,5287,5289],{"class":71,"line":210},[69,5288,3624],{"class":82},[69,5290,190],{"class":86},[69,5292,5293],{"class":71,"line":225},[69,5294,100],{"emptyLinePlaceholder":99},[69,5296,5297,5299,5301,5304,5306,5308],{"class":71,"line":240},[69,5298,5191],{"class":143},[69,5300,147],{"class":86},[69,5302,5303],{"class":155},"\"handles empty string\"",[69,5305,39],{"class":86},[69,5307,106],{"class":82},[69,5309,1702],{"class":86},[69,5311,5312,5314,5316,5318,5320,5322,5324,5326,5328,5330,5332,5334],{"class":71,"line":256},[69,5313,3593],{"class":143},[69,5315,113],{"class":86},[69,5317,5211],{"class":109},[69,5319,113],{"class":86},[69,5321,5216],{"class":143},[69,5323,147],{"class":86},[69,5325,2227],{"class":155},[69,5327,5223],{"class":86},[69,5329,2177],{"class":143},[69,5331,147],{"class":86},[69,5333,2227],{"class":155},[69,5335,1632],{"class":86},[69,5337,5338,5340],{"class":71,"line":280},[69,5339,3624],{"class":82},[69,5341,190],{"class":86},[69,5343,5344],{"class":71,"line":285},[69,5345,100],{"emptyLinePlaceholder":99},[69,5347,5348,5350,5352,5355,5357,5359],{"class":71,"line":290},[69,5349,5191],{"class":143},[69,5351,147],{"class":86},[69,5353,5354],{"class":155},"\"handles all-whitespace string\"",[69,5356,39],{"class":86},[69,5358,106],{"class":82},[69,5360,1702],{"class":86},[69,5362,5363,5365,5367,5369,5371,5373,5375,5377,5379,5381,5383,5386],{"class":71,"line":295},[69,5364,3593],{"class":143},[69,5366,113],{"class":86},[69,5368,5211],{"class":109},[69,5370,113],{"class":86},[69,5372,5216],{"class":143},[69,5374,147],{"class":86},[69,5376,2227],{"class":155},[69,5378,5223],{"class":86},[69,5380,2177],{"class":143},[69,5382,147],{"class":86},[69,5384,5385],{"class":155},"\"   \"",[69,5387,1632],{"class":86},[69,5389,5390,5392],{"class":71,"line":310},[69,5391,3624],{"class":82},[69,5393,190],{"class":86},[69,5395,5396,5398],{"class":71,"line":325},[69,5397,1668],{"class":82},[69,5399,190],{"class":86},[69,5401,5402],{"class":71,"line":341},[69,5403,100],{"emptyLinePlaceholder":99},[69,5405,5406,5408,5410,5413,5415,5417],{"class":71,"line":360},[69,5407,5175],{"class":143},[69,5409,147],{"class":86},[69,5411,5412],{"class":155},"\"string_utils.split\"",[69,5414,39],{"class":86},[69,5416,106],{"class":82},[69,5418,1702],{"class":86},[69,5420,5421,5423,5425,5428,5430,5432],{"class":71,"line":365},[69,5422,5191],{"class":143},[69,5424,147],{"class":86},[69,5426,5427],{"class":155},"\"splits on delimiter\"",[69,5429,39],{"class":86},[69,5431,106],{"class":82},[69,5433,1702],{"class":86},[69,5435,5436,5438,5440,5442,5444,5447,5449,5451,5453,5455,5457,5459,5461,5463,5465,5467,5469,5471],{"class":71,"line":370},[69,5437,3593],{"class":143},[69,5439,113],{"class":86},[69,5441,5211],{"class":109},[69,5443,113],{"class":86},[69,5445,5446],{"class":143},"same",[69,5448,2224],{"class":86},[69,5450,2284],{"class":155},[69,5452,39],{"class":86},[69,5454,2289],{"class":155},[69,5456,39],{"class":86},[69,5458,2294],{"class":155},[69,5460,2230],{"class":86},[69,5462,2233],{"class":143},[69,5464,147],{"class":86},[69,5466,2303],{"class":155},[69,5468,39],{"class":86},[69,5470,2242],{"class":155},[69,5472,1632],{"class":86},[69,5474,5475,5477],{"class":71,"line":375},[69,5476,3624],{"class":82},[69,5478,190],{"class":86},[69,5480,5481],{"class":71,"line":389},[69,5482,100],{"emptyLinePlaceholder":99},[69,5484,5485,5487,5489,5492,5494,5496],{"class":71,"line":402},[69,5486,5191],{"class":143},[69,5488,147],{"class":86},[69,5490,5491],{"class":155},"\"returns single element for no delimiter\"",[69,5493,39],{"class":86},[69,5495,106],{"class":82},[69,5497,1702],{"class":86},[69,5499,5500,5502,5504,5506,5508,5510,5512,5514,5516,5518,5520,5522,5524,5526],{"class":71,"line":421},[69,5501,3593],{"class":143},[69,5503,113],{"class":86},[69,5505,5211],{"class":109},[69,5507,113],{"class":86},[69,5509,5446],{"class":143},[69,5511,2224],{"class":86},[69,5513,2415],{"class":155},[69,5515,2230],{"class":86},[69,5517,2233],{"class":143},[69,5519,147],{"class":86},[69,5521,2415],{"class":155},[69,5523,39],{"class":86},[69,5525,2242],{"class":155},[69,5527,1632],{"class":86},[69,5529,5530,5532],{"class":71,"line":426},[69,5531,3624],{"class":82},[69,5533,190],{"class":86},[69,5535,5536],{"class":71,"line":431},[69,5537,100],{"emptyLinePlaceholder":99},[69,5539,5540,5542,5544,5547,5549,5551],{"class":71,"line":436},[69,5541,5191],{"class":143},[69,5543,147],{"class":86},[69,5545,5546],{"class":155},"\"handles trailing delimiter\"",[69,5548,39],{"class":86},[69,5550,106],{"class":82},[69,5552,1702],{"class":86},[69,5554,5555,5557,5559,5561,5563,5565,5567,5569,5571,5573,5575,5577,5579,5581,5583,5585,5587,5589,5591,5593],{"class":71,"line":450},[69,5556,3593],{"class":143},[69,5558,113],{"class":86},[69,5560,5211],{"class":109},[69,5562,113],{"class":86},[69,5564,5446],{"class":143},[69,5566,2224],{"class":86},[69,5568,2284],{"class":155},[69,5570,39],{"class":86},[69,5572,2289],{"class":155},[69,5574,39],{"class":86},[69,5576,2294],{"class":155},[69,5578,39],{"class":86},[69,5580,2227],{"class":155},[69,5582,2230],{"class":86},[69,5584,2233],{"class":143},[69,5586,147],{"class":86},[69,5588,2369],{"class":155},[69,5590,39],{"class":86},[69,5592,2242],{"class":155},[69,5594,1632],{"class":86},[69,5596,5597,5599],{"class":71,"line":468},[69,5598,3624],{"class":82},[69,5600,190],{"class":86},[69,5602,5603,5605],{"class":71,"line":484},[69,5604,1668],{"class":82},[69,5606,190],{"class":86},[51,5608,5610],{"id":5609},"tips-tricks","💡 Tips & Tricks",[15,5612,5613,5616],{},[32,5614,5615],{},"Test the contract, not the implementation",": Assert outputs and side effects, not internal state. This makes tests resilient to refactoring.",[15,5618,5619,5626],{},[32,5620,5621,5622,5625],{},"Use ",[19,5623,5624],{},"assert.raises"," to test error paths",": Don't just test the happy path — test that invalid inputs raise the expected errors.",[15,5628,5629,5632],{},[32,5630,5631],{},"Mock at the boundaries (I\u002FO, network, db)",": Don't mock pure functions. Mock things with external side effects.",[15,5634,5635,5638],{},[32,5636,5637],{},"One assertion per test (mostly)",": If a test fails, you know exactly which condition. Multiple assertions make failure messages ambiguous.",[51,5640,5642],{"id":5641},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[15,5644,5645,5655,5656,5658],{},[32,5646,5647,5650,5651,5654],{},[19,5648,5649],{},"assert.are.same"," (busted) does deep comparison; ",[19,5652,5653],{},"assert.are.equal"," does shallow",": Don't mix them up — ",[19,5657,5216],{}," on tables checks identity.",[15,5660,5661,5668],{},[32,5662,5663,5664,5667],{},"Mocking ",[19,5665,5666],{},"io.open"," affects ALL code, not just the module under test",": Be careful — your test framework may break if it can't read files. Scope mocks to the test with setup\u002Fteardown.",[56,5670,5671],{"language":58},[61,5672,5674],{"className":63,"code":5673,"language":58,"meta":65,"style":65},"-- BAD: global mock breaks everything\nlocal real_open = io.open\nio.open = mock_open  -- affects the test runner too!\n-- ... run tests ...\nio.open = real_open  -- restore (but if tests error before here, it stays mocked)\n\n-- GOOD: mock via package.preload or dependency injection\n-- (the module under test gets the mock; the test framework uses the real io.open)\n",[19,5675,5676,5681,5693,5709,5714,5728,5732,5737],{"__ignoreMap":65},[69,5677,5678],{"class":71,"line":72},[69,5679,5680],{"class":75},"-- BAD: global mock breaks everything\n",[69,5682,5683,5685,5688,5690],{"class":71,"line":79},[69,5684,83],{"class":82},[69,5686,5687],{"class":86}," real_open ",[69,5689,90],{"class":82},[69,5691,5692],{"class":143}," io.open\n",[69,5694,5695,5698,5701,5703,5706],{"class":71,"line":96},[69,5696,5697],{"class":86},"io.",[69,5699,5700],{"class":109},"open",[69,5702,1164],{"class":82},[69,5704,5705],{"class":86}," mock_open  ",[69,5707,5708],{"class":75},"-- affects the test runner too!\n",[69,5710,5711],{"class":71,"line":103},[69,5712,5713],{"class":75},"-- ... run tests ...\n",[69,5715,5716,5718,5720,5722,5725],{"class":71,"line":122},[69,5717,5697],{"class":86},[69,5719,5700],{"class":109},[69,5721,1164],{"class":82},[69,5723,5724],{"class":86}," real_open  ",[69,5726,5727],{"class":75},"-- restore (but if tests error before here, it stays mocked)\n",[69,5729,5730],{"class":71,"line":140},[69,5731,100],{"emptyLinePlaceholder":99},[69,5733,5734],{"class":71,"line":162},[69,5735,5736],{"class":75},"-- GOOD: mock via package.preload or dependency injection\n",[69,5738,5739],{"class":71,"line":193},[69,5740,5741],{"class":75},"-- (the module under test gets the mock; the test framework uses the real io.open)\n",[15,5743,5744,5750,5751,5754],{},[32,5745,5746,5749],{},[19,5747,5748],{},"pcall"," catches errors, but the stack is lost",": For testing error messages, use the error value (second return from pcall). For testing stack traces, use ",[19,5752,5753],{},"xpcall"," with a handler.",[51,5756,5758],{"id":5757},"spot-the-bug","🧠 Spot the Bug",[56,5760,5761,5883],{"language":58},[61,5762,5764],{"className":63,"code":5763,"language":58,"meta":65,"style":65},"-- test_split.lua\nlocal su = require(\"string_utils\")\n\nlocal function test_split()\n  local result = su.split(\"a,b,c\", \",\")\n  assert(result == {\"a\", \"b\", \"c\"}, \"split should return array\")\nend\n\ntest_split()\nprint(\"passed\")\n",[19,5765,5766,5771,5787,5791,5802,5826,5856,5860,5864,5871],{"__ignoreMap":65},[69,5767,5768],{"class":71,"line":72},[69,5769,5770],{"class":75},"-- test_split.lua\n",[69,5772,5773,5775,5777,5779,5781,5783,5785],{"class":71,"line":79},[69,5774,83],{"class":82},[69,5776,1898],{"class":86},[69,5778,90],{"class":82},[69,5780,1131],{"class":143},[69,5782,147],{"class":86},[69,5784,1907],{"class":155},[69,5786,190],{"class":86},[69,5788,5789],{"class":71,"line":96},[69,5790,100],{"emptyLinePlaceholder":99},[69,5792,5793,5795,5797,5800],{"class":71,"line":103},[69,5794,83],{"class":82},[69,5796,2601],{"class":82},[69,5798,5799],{"class":109}," test_split",[69,5801,1702],{"class":86},[69,5803,5804,5806,5809,5811,5814,5816,5818,5820,5822,5824],{"class":71,"line":122},[69,5805,622],{"class":82},[69,5807,5808],{"class":86}," result ",[69,5810,90],{"class":82},[69,5812,5813],{"class":86}," su.",[69,5815,2233],{"class":143},[69,5817,147],{"class":86},[69,5819,2303],{"class":155},[69,5821,39],{"class":86},[69,5823,2242],{"class":155},[69,5825,190],{"class":86},[69,5827,5828,5831,5834,5836,5839,5841,5843,5845,5847,5849,5851,5854],{"class":71,"line":140},[69,5829,5830],{"class":143},"  assert",[69,5832,5833],{"class":86},"(result ",[69,5835,233],{"class":82},[69,5837,5838],{"class":86}," {",[69,5840,2284],{"class":155},[69,5842,39],{"class":86},[69,5844,2289],{"class":155},[69,5846,39],{"class":86},[69,5848,2294],{"class":155},[69,5850,2159],{"class":86},[69,5852,5853],{"class":155},"\"split should return array\"",[69,5855,190],{"class":86},[69,5857,5858],{"class":71,"line":162},[69,5859,202],{"class":82},[69,5861,5862],{"class":71,"line":193},[69,5863,100],{"emptyLinePlaceholder":99},[69,5865,5866,5869],{"class":71,"line":199},[69,5867,5868],{"class":143},"test_split",[69,5870,1702],{"class":86},[69,5872,5873,5876,5878,5881],{"class":71,"line":205},[69,5874,5875],{"class":143},"print",[69,5877,147],{"class":86},[69,5879,5880],{"class":155},"\"passed\"",[69,5882,190],{"class":86},[5884,5885,5886,5889,5914,5921,5952,5955],"details",{},[1699,5887,5888],{},"Answer",[15,5890,5891,5892,5895,5896,5899,5900,5903,5904,5906,5907,5909,5910,5913],{},"The assertion ",[19,5893,5894],{},"result == {\"a\", \"b\", \"c\"}"," compares a table to a ",[32,5897,5898],{},"newly constructed table",". In Lua, table comparison is by ",[32,5901,5902],{},"identity"," (reference), not content. Two different table objects are never ",[19,5905,233],{},", even if they contain the same elements. So ",[19,5908,5894],{}," is always ",[19,5911,5912],{},"false",", and the assertion always fails.",[15,5915,5916,5917,5920],{},"Fix: use a deep equality check (like ",[19,5918,5919],{},"Assert.deep_equals","):",[61,5922,5924],{"className":63,"code":5923,"language":58,"meta":65,"style":65},"Assert.deep_equals({\"a\", \"b\", \"c\"}, result, \"split should return array\")\n",[19,5925,5926],{"__ignoreMap":65},[69,5927,5928,5931,5933,5935,5937,5939,5941,5943,5945,5948,5950],{"class":71,"line":72},[69,5929,5930],{"class":86},"Assert.",[69,5932,762],{"class":143},[69,5934,2224],{"class":86},[69,5936,2284],{"class":155},[69,5938,39],{"class":86},[69,5940,2289],{"class":155},[69,5942,39],{"class":86},[69,5944,2294],{"class":155},[69,5946,5947],{"class":86},"}, result, ",[69,5949,5853],{"class":155},[69,5951,190],{"class":86},[15,5953,5954],{},"Or compare element by element:",[61,5956,5958],{"className":63,"code":5957,"language":58,"meta":65,"style":65},"assert(result[1] == \"a\" and result[2] == \"b\" and result[3] == \"c\", \"split failed\")\n",[19,5959,5960],{"__ignoreMap":65},[69,5961,5962,5964,5967,5969,5971,5973,5976,5978,5981,5983,5985,5987,5990,5992,5994,5996,5998,6000,6003,6005,6008],{"class":71,"line":72},[69,5963,3685],{"class":143},[69,5965,5966],{"class":86},"(result[",[69,5968,2623],{"class":143},[69,5970,1513],{"class":86},[69,5972,233],{"class":82},[69,5974,5975],{"class":155}," \"a\" ",[69,5977,677],{"class":82},[69,5979,5980],{"class":86}," result[",[69,5982,187],{"class":143},[69,5984,1513],{"class":86},[69,5986,233],{"class":82},[69,5988,5989],{"class":155}," \"b\" ",[69,5991,677],{"class":82},[69,5993,5980],{"class":86},[69,5995,3969],{"class":143},[69,5997,1513],{"class":86},[69,5999,233],{"class":82},[69,6001,6002],{"class":155}," \"c\"",[69,6004,39],{"class":86},[69,6006,6007],{"class":155},"\"split failed\"",[69,6009,190],{"class":86},[6011,6012,6013],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}",{"title":65,"searchDepth":79,"depth":79,"links":6015},[6016,6017,6018,6019,6021,6022,6023,6024],{"id":53,"depth":79,"text":54},{"id":1106,"depth":79,"text":1107},{"id":1847,"depth":79,"text":1848},{"id":2531,"depth":79,"text":6020},"Mocking with package.preload",{"id":3362,"depth":79,"text":3363},{"id":5609,"depth":79,"text":5610},{"id":5641,"depth":79,"text":5642},{"id":5757,"depth":79,"text":5758},"[object Object]","md",{},"\u002Flua\u002F14-testing",{"title":5,"description":6030},{"Deep-dive into testing Lua":6031},"assertion-based testing with luaunit\u002Fbusted, test structure (setup\u002Fteardown\u002Fparametric), mocking via package.preload, dependency injection for testability, table-based tests, property-based testing patterns, and coverage measurement with debug hooks.","lua\u002F14-testing","ILkUdsxkdh1tL0VlTt1dGF1-RwFIFzuL_DXyPf_I-yM",1789924650619]