[{"data":1,"prerenderedAt":5194},["ShallowReactive",2],{"page-\u002Fbash\u002F14-testing":3},{"id":4,"title":5,"body":6,"description":5187,"extension":5188,"meta":5189,"navigation":136,"path":5190,"seo":5191,"stem":5192,"__hash__":5193},"content\u002Fbash\u002F14-testing.md","Bash 14 — Testing Architecture: bats, Mocking & CI Integration",{"type":7,"value":8,"toc":5171},"minimark",[9,14,19,570,584,1139,1143,1764,1768,2151,2155,2634,2638,3062,3066,3376,3380,3580,3584,3856,3860,4314,4318,4751,4755,4886,4890,4894,4944,5167],[10,11,13],"h1",{"id":12},"_14-testing-architecture-bats-mocking-ci-integration","14 — Testing Architecture: bats, Mocking & CI Integration",[15,16,18],"h2",{"id":17},"bats-setup-and-test-structure","bats Setup and Test Structure",[20,21,23],"code-wrapper",{"language":22},"bash",[24,25,29],"pre",{"className":26,"code":27,"language":22,"meta":28,"style":28},"language-bash shiki shiki-themes github-light github-dark","# ── Install bats-core ──\nbrew install bats-core          # macOS\nnpm install -g bats             # npm\napt install bats                # Debian\u002FUbuntu (may be older)\n# From source (latest):\ngit clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-core.git\ncd bats-core && .\u002Finstall.sh \"$HOME\u002F.local\"\n\n# ── Optional: bats-assert and bats-support (for assertion helpers) ──\ngit clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-support.git test\u002Ftest_helper\u002Fbats-support\ngit clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-assert.git test\u002Ftest_helper\u002Fbats-assert\n\n# ── Test file structure (.bats extension) ──\n# test\u002Fmath_test.bats:\n#!\u002Fusr\u002Fbin\u002Fenv bats\n\n# ── Setup\u002Fteardown: run before\u002Fafter EACH test ──\nsetup() {\n    # Create test fixtures\n    tmpdir=$(mktemp -d)\n    echo \"3.14159\" > \"$tmpdir\u002Fpi\"\n    echo \"hello world\" > \"$tmpdir\u002Fdata\"\n}\n\nteardown() {\n    # Clean up (runs even if the test fails)\n    [[ -n \"$tmpdir\" && -d \"$tmpdir\" ]] && rm -rf -- \"$tmpdir\"\n}\n\n# ── Test: @test \"description\" { body } ──\n@test \"string length\" {\n    str=\"hello\"\n    [[ \"${#str}\" -eq 5 ]]\n}\n\n@test \"uppercase\" {\n    result=$(echo 'hello' | tr 'a-z' 'A-Z')\n    [[ \"$result\" == \"HELLO\" ]]\n}\n\n@test \"file exists after setup\" {\n    [[ -f \"$tmpdir\u002Fpi\" ]]\n}\n\n@test \"file content is correct\" {\n    [[ \"$(cat \"$tmpdir\u002Fdata\")\" == \"hello world\" ]]\n}\n","",[30,31,32,41,58,76,89,95,107,131,138,144,157,170,175,181,187,193,198,204,213,219,241,261,278,284,289,297,303,349,354,359,365,377,388,414,419,424,434,464,484,489,494,504,521,526,531,541,565],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sdCPZ","# ── Install bats-core ──\n",[33,42,44,48,52,55],{"class":35,"line":43},2,[33,45,47],{"class":46},"sIsaT","brew",[33,49,51],{"class":50},"sJ6F3"," install",[33,53,54],{"class":50}," bats-core",[33,56,57],{"class":39},"          # macOS\n",[33,59,61,64,66,70,73],{"class":35,"line":60},3,[33,62,63],{"class":46},"npm",[33,65,51],{"class":50},[33,67,69],{"class":68},"snvgF"," -g",[33,71,72],{"class":50}," bats",[33,74,75],{"class":39},"             # npm\n",[33,77,79,82,84,86],{"class":35,"line":78},4,[33,80,81],{"class":46},"apt",[33,83,51],{"class":50},[33,85,72],{"class":50},[33,87,88],{"class":39},"                # Debian\u002FUbuntu (may be older)\n",[33,90,92],{"class":35,"line":91},5,[33,93,94],{"class":39},"# From source (latest):\n",[33,96,98,101,104],{"class":35,"line":97},6,[33,99,100],{"class":46},"git",[33,102,103],{"class":50}," clone",[33,105,106],{"class":50}," https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-core.git\n",[33,108,110,113,115,119,122,125,128],{"class":35,"line":109},7,[33,111,112],{"class":68},"cd",[33,114,54],{"class":50},[33,116,118],{"class":117},"ssxIu"," && ",[33,120,121],{"class":46},".\u002Finstall.sh",[33,123,124],{"class":50}," \"",[33,126,127],{"class":117},"$HOME",[33,129,130],{"class":50},"\u002F.local\"\n",[33,132,134],{"class":35,"line":133},8,[33,135,137],{"emptyLinePlaceholder":136},true,"\n",[33,139,141],{"class":35,"line":140},9,[33,142,143],{"class":39},"# ── Optional: bats-assert and bats-support (for assertion helpers) ──\n",[33,145,147,149,151,154],{"class":35,"line":146},10,[33,148,100],{"class":46},[33,150,103],{"class":50},[33,152,153],{"class":50}," https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-support.git",[33,155,156],{"class":50}," test\u002Ftest_helper\u002Fbats-support\n",[33,158,160,162,164,167],{"class":35,"line":159},11,[33,161,100],{"class":46},[33,163,103],{"class":50},[33,165,166],{"class":50}," https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-assert.git",[33,168,169],{"class":50}," test\u002Ftest_helper\u002Fbats-assert\n",[33,171,173],{"class":35,"line":172},12,[33,174,137],{"emptyLinePlaceholder":136},[33,176,178],{"class":35,"line":177},13,[33,179,180],{"class":39},"# ── Test file structure (.bats extension) ──\n",[33,182,184],{"class":35,"line":183},14,[33,185,186],{"class":39},"# test\u002Fmath_test.bats:\n",[33,188,190],{"class":35,"line":189},15,[33,191,192],{"class":39},"#!\u002Fusr\u002Fbin\u002Fenv bats\n",[33,194,196],{"class":35,"line":195},16,[33,197,137],{"emptyLinePlaceholder":136},[33,199,201],{"class":35,"line":200},17,[33,202,203],{"class":39},"# ── Setup\u002Fteardown: run before\u002Fafter EACH test ──\n",[33,205,207,210],{"class":35,"line":206},18,[33,208,209],{"class":46},"setup",[33,211,212],{"class":117},"() {\n",[33,214,216],{"class":35,"line":215},19,[33,217,218],{"class":39},"    # Create test fixtures\n",[33,220,222,225,229,232,235,238],{"class":35,"line":221},20,[33,223,224],{"class":117},"    tmpdir",[33,226,228],{"class":227},"svdQ7","=",[33,230,231],{"class":117},"$(",[33,233,234],{"class":46},"mktemp",[33,236,237],{"class":68}," -d",[33,239,240],{"class":117},")\n",[33,242,244,247,250,253,255,258],{"class":35,"line":243},21,[33,245,246],{"class":68},"    echo",[33,248,249],{"class":50}," \"3.14159\"",[33,251,252],{"class":227}," >",[33,254,124],{"class":50},[33,256,257],{"class":117},"$tmpdir",[33,259,260],{"class":50},"\u002Fpi\"\n",[33,262,264,266,269,271,273,275],{"class":35,"line":263},22,[33,265,246],{"class":68},[33,267,268],{"class":50}," \"hello world\"",[33,270,252],{"class":227},[33,272,124],{"class":50},[33,274,257],{"class":117},[33,276,277],{"class":50},"\u002Fdata\"\n",[33,279,281],{"class":35,"line":280},23,[33,282,283],{"class":117},"}\n",[33,285,287],{"class":35,"line":286},24,[33,288,137],{"emptyLinePlaceholder":136},[33,290,292,295],{"class":35,"line":291},25,[33,293,294],{"class":46},"teardown",[33,296,212],{"class":117},[33,298,300],{"class":35,"line":299},26,[33,301,302],{"class":39},"    # Clean up (runs even if the test fails)\n",[33,304,306,309,312,314,316,319,322,324,326,328,330,333,336,339,342,344,346],{"class":35,"line":305},27,[33,307,308],{"class":117},"    [[ ",[33,310,311],{"class":227},"-n",[33,313,124],{"class":50},[33,315,257],{"class":117},[33,317,318],{"class":50},"\"",[33,320,321],{"class":227}," &&",[33,323,237],{"class":227},[33,325,124],{"class":50},[33,327,257],{"class":117},[33,329,318],{"class":50},[33,331,332],{"class":117}," ]] && ",[33,334,335],{"class":46},"rm",[33,337,338],{"class":68}," -rf",[33,340,341],{"class":68}," --",[33,343,124],{"class":50},[33,345,257],{"class":117},[33,347,348],{"class":50},"\"\n",[33,350,352],{"class":35,"line":351},28,[33,353,283],{"class":117},[33,355,357],{"class":35,"line":356},29,[33,358,137],{"emptyLinePlaceholder":136},[33,360,362],{"class":35,"line":361},30,[33,363,364],{"class":39},"# ── Test: @test \"description\" { body } ──\n",[33,366,368,371,374],{"class":35,"line":367},31,[33,369,370],{"class":46},"@test",[33,372,373],{"class":50}," \"string length\"",[33,375,376],{"class":50}," {\n",[33,378,380,383,385],{"class":35,"line":379},32,[33,381,382],{"class":117},"    str",[33,384,228],{"class":227},[33,386,387],{"class":50},"\"hello\"\n",[33,389,391,393,396,399,402,405,408,411],{"class":35,"line":390},33,[33,392,308],{"class":117},[33,394,395],{"class":50},"\"${",[33,397,398],{"class":227},"#",[33,400,401],{"class":117},"str",[33,403,404],{"class":50},"}\"",[33,406,407],{"class":227}," -eq",[33,409,410],{"class":68}," 5",[33,412,413],{"class":117}," ]]\n",[33,415,417],{"class":35,"line":416},34,[33,418,283],{"class":117},[33,420,422],{"class":35,"line":421},35,[33,423,137],{"emptyLinePlaceholder":136},[33,425,427,429,432],{"class":35,"line":426},36,[33,428,370],{"class":46},[33,430,431],{"class":50}," \"uppercase\"",[33,433,376],{"class":50},[33,435,437,440,442,444,447,450,453,456,459,462],{"class":35,"line":436},37,[33,438,439],{"class":117},"    result",[33,441,228],{"class":227},[33,443,231],{"class":117},[33,445,446],{"class":68},"echo",[33,448,449],{"class":50}," 'hello'",[33,451,452],{"class":227}," |",[33,454,455],{"class":46}," tr",[33,457,458],{"class":50}," 'a-z'",[33,460,461],{"class":50}," 'A-Z'",[33,463,240],{"class":117},[33,465,467,469,471,474,476,479,482],{"class":35,"line":466},38,[33,468,308],{"class":117},[33,470,318],{"class":50},[33,472,473],{"class":117},"$result",[33,475,318],{"class":50},[33,477,478],{"class":227}," ==",[33,480,481],{"class":50}," \"HELLO\"",[33,483,413],{"class":117},[33,485,487],{"class":35,"line":486},39,[33,488,283],{"class":117},[33,490,492],{"class":35,"line":491},40,[33,493,137],{"emptyLinePlaceholder":136},[33,495,497,499,502],{"class":35,"line":496},41,[33,498,370],{"class":46},[33,500,501],{"class":50}," \"file exists after setup\"",[33,503,376],{"class":50},[33,505,507,509,512,514,516,519],{"class":35,"line":506},42,[33,508,308],{"class":117},[33,510,511],{"class":227},"-f",[33,513,124],{"class":50},[33,515,257],{"class":117},[33,517,518],{"class":50},"\u002Fpi\"",[33,520,413],{"class":117},[33,522,524],{"class":35,"line":523},43,[33,525,283],{"class":117},[33,527,529],{"class":35,"line":528},44,[33,530,137],{"emptyLinePlaceholder":136},[33,532,534,536,539],{"class":35,"line":533},45,[33,535,370],{"class":46},[33,537,538],{"class":50}," \"file content is correct\"",[33,540,376],{"class":50},[33,542,544,546,549,552,554,556,559,561,563],{"class":35,"line":543},46,[33,545,308],{"class":117},[33,547,548],{"class":50},"\"$(",[33,550,551],{"class":46},"cat",[33,553,124],{"class":50},[33,555,257],{"class":117},[33,557,558],{"class":50},"\u002Fdata\")\"",[33,560,478],{"class":227},[33,562,268],{"class":50},[33,564,413],{"class":117},[33,566,568],{"class":35,"line":567},47,[33,569,283],{"class":117},[15,571,573,576,577,576,580,583],{"id":572},"run-status-output-testing-commands",[30,574,575],{},"run",", ",[30,578,579],{},"$status",[30,581,582],{},"$output",": Testing Commands",[20,585,586],{"language":22},[24,587,589],{"className":26,"code":588,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bats\n\n# ── `run` executes a command and captures: ──\n# $status  — exit code (0-255)\n# $output  — combined stdout+stderr\n# $lines   — array of output lines\n\n@test \"command succeeds\" {\n    run echo \"hello\"\n    [[ \"$status\" -eq 0 ]]\n    [[ \"$output\" == \"hello\" ]]\n}\n\n@test \"command fails with specific exit code\" {\n    run bash -c 'exit 42'\n    [[ \"$status\" -eq 42 ]]\n}\n\n@test \"command outputs multiple lines\" {\n    run printf '%s\\n' \"line1\" \"line2\" \"line3\"\n    [[ \"$status\" -eq 0 ]]\n    [[ \"${#lines[@]}\" -eq 3 ]]\n    [[ \"${lines[0]}\" == \"line1\" ]]\n    [[ \"${lines[1]}\" == \"line2\" ]]\n    [[ \"${lines[2]}\" == \"line3\" ]]\n}\n\n@test \"command outputs contains substring\" {\n    run echo \"error: file not found\"\n    [[ \"$status\" -eq 0 ]]\n    [[ \"$output\" == *\"file not found\"* ]]   # glob match (substring)\n}\n\n@test \"command outputs matches regex\" {\n    run echo \"2024-01-15\"\n    [[ \"$output\" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]\n}\n\n# ── ⚠️ `run` captures stdout AND stderr together ──\n@test \"stderr is captured\" {\n    run bash -c 'echo \"stdout\"; echo \"stderr\" >&2'\n    [[ \"$output\" == *stdout* ]]\n    [[ \"$output\" == *stderr* ]]   # both captured (order may vary)\n}\n\n# ── Separate stderr (bats-core 1.5+) ──\n@test \"separate stderr\" {\n    run --separate-stderr bash -c 'echo \"out\"; echo \"err\" >&2'\n    [[ \"$output\" == \"out\" ]]\n    [[ \"$stderr\" == \"err\" ]]\n}\n",[30,590,591,595,599,604,609,614,619,623,632,643,660,677,681,685,694,707,724,728,732,741,760,776,803,820,837,855,859,863,872,881,897,924,928,932,941,950,982,986,990,995,1004,1015,1036,1060,1064,1068,1073,1082,1097,1115,1134],{"__ignoreMap":28},[33,592,593],{"class":35,"line":36},[33,594,192],{"class":39},[33,596,597],{"class":35,"line":43},[33,598,137],{"emptyLinePlaceholder":136},[33,600,601],{"class":35,"line":60},[33,602,603],{"class":39},"# ── `run` executes a command and captures: ──\n",[33,605,606],{"class":35,"line":78},[33,607,608],{"class":39},"# $status  — exit code (0-255)\n",[33,610,611],{"class":35,"line":91},[33,612,613],{"class":39},"# $output  — combined stdout+stderr\n",[33,615,616],{"class":35,"line":97},[33,617,618],{"class":39},"# $lines   — array of output lines\n",[33,620,621],{"class":35,"line":109},[33,622,137],{"emptyLinePlaceholder":136},[33,624,625,627,630],{"class":35,"line":133},[33,626,370],{"class":46},[33,628,629],{"class":50}," \"command succeeds\"",[33,631,376],{"class":50},[33,633,634,637,640],{"class":35,"line":140},[33,635,636],{"class":46},"    run",[33,638,639],{"class":50}," echo",[33,641,642],{"class":50}," \"hello\"\n",[33,644,645,647,649,651,653,655,658],{"class":35,"line":146},[33,646,308],{"class":117},[33,648,318],{"class":50},[33,650,579],{"class":117},[33,652,318],{"class":50},[33,654,407],{"class":227},[33,656,657],{"class":68}," 0",[33,659,413],{"class":117},[33,661,662,664,666,668,670,672,675],{"class":35,"line":159},[33,663,308],{"class":117},[33,665,318],{"class":50},[33,667,582],{"class":117},[33,669,318],{"class":50},[33,671,478],{"class":227},[33,673,674],{"class":50}," \"hello\"",[33,676,413],{"class":117},[33,678,679],{"class":35,"line":172},[33,680,283],{"class":117},[33,682,683],{"class":35,"line":177},[33,684,137],{"emptyLinePlaceholder":136},[33,686,687,689,692],{"class":35,"line":183},[33,688,370],{"class":46},[33,690,691],{"class":50}," \"command fails with specific exit code\"",[33,693,376],{"class":50},[33,695,696,698,701,704],{"class":35,"line":189},[33,697,636],{"class":46},[33,699,700],{"class":50}," bash",[33,702,703],{"class":68}," -c",[33,705,706],{"class":50}," 'exit 42'\n",[33,708,709,711,713,715,717,719,722],{"class":35,"line":195},[33,710,308],{"class":117},[33,712,318],{"class":50},[33,714,579],{"class":117},[33,716,318],{"class":50},[33,718,407],{"class":227},[33,720,721],{"class":68}," 42",[33,723,413],{"class":117},[33,725,726],{"class":35,"line":200},[33,727,283],{"class":117},[33,729,730],{"class":35,"line":206},[33,731,137],{"emptyLinePlaceholder":136},[33,733,734,736,739],{"class":35,"line":215},[33,735,370],{"class":46},[33,737,738],{"class":50}," \"command outputs multiple lines\"",[33,740,376],{"class":50},[33,742,743,745,748,751,754,757],{"class":35,"line":221},[33,744,636],{"class":46},[33,746,747],{"class":50}," printf",[33,749,750],{"class":50}," '%s\\n'",[33,752,753],{"class":50}," \"line1\"",[33,755,756],{"class":50}," \"line2\"",[33,758,759],{"class":50}," \"line3\"\n",[33,761,762,764,766,768,770,772,774],{"class":35,"line":243},[33,763,308],{"class":117},[33,765,318],{"class":50},[33,767,579],{"class":117},[33,769,318],{"class":50},[33,771,407],{"class":227},[33,773,657],{"class":68},[33,775,413],{"class":117},[33,777,778,780,782,784,787,790,793,796,798,801],{"class":35,"line":263},[33,779,308],{"class":117},[33,781,395],{"class":50},[33,783,398],{"class":227},[33,785,786],{"class":117},"lines",[33,788,789],{"class":50},"[",[33,791,792],{"class":227},"@",[33,794,795],{"class":50},"]}\"",[33,797,407],{"class":227},[33,799,800],{"class":68}," 3",[33,802,413],{"class":117},[33,804,805,807,809,811,814,816,818],{"class":35,"line":280},[33,806,308],{"class":117},[33,808,395],{"class":50},[33,810,786],{"class":117},[33,812,813],{"class":50},"[0]}\"",[33,815,478],{"class":227},[33,817,753],{"class":50},[33,819,413],{"class":117},[33,821,822,824,826,828,831,833,835],{"class":35,"line":286},[33,823,308],{"class":117},[33,825,395],{"class":50},[33,827,786],{"class":117},[33,829,830],{"class":50},"[1]}\"",[33,832,478],{"class":227},[33,834,756],{"class":50},[33,836,413],{"class":117},[33,838,839,841,843,845,848,850,853],{"class":35,"line":291},[33,840,308],{"class":117},[33,842,395],{"class":50},[33,844,786],{"class":117},[33,846,847],{"class":50},"[2]}\"",[33,849,478],{"class":227},[33,851,852],{"class":50}," \"line3\"",[33,854,413],{"class":117},[33,856,857],{"class":35,"line":299},[33,858,283],{"class":117},[33,860,861],{"class":35,"line":305},[33,862,137],{"emptyLinePlaceholder":136},[33,864,865,867,870],{"class":35,"line":351},[33,866,370],{"class":46},[33,868,869],{"class":50}," \"command outputs contains substring\"",[33,871,376],{"class":50},[33,873,874,876,878],{"class":35,"line":356},[33,875,636],{"class":46},[33,877,639],{"class":50},[33,879,880],{"class":50}," \"error: file not found\"\n",[33,882,883,885,887,889,891,893,895],{"class":35,"line":361},[33,884,308],{"class":117},[33,886,318],{"class":50},[33,888,579],{"class":117},[33,890,318],{"class":50},[33,892,407],{"class":227},[33,894,657],{"class":68},[33,896,413],{"class":117},[33,898,899,901,903,905,907,909,912,915,918,921],{"class":35,"line":367},[33,900,308],{"class":117},[33,902,318],{"class":50},[33,904,582],{"class":117},[33,906,318],{"class":50},[33,908,478],{"class":227},[33,910,911],{"class":227}," *",[33,913,914],{"class":50},"\"file not found\"",[33,916,917],{"class":227},"*",[33,919,920],{"class":117}," ]]   ",[33,922,923],{"class":39},"# glob match (substring)\n",[33,925,926],{"class":35,"line":379},[33,927,283],{"class":117},[33,929,930],{"class":35,"line":390},[33,931,137],{"emptyLinePlaceholder":136},[33,933,934,936,939],{"class":35,"line":416},[33,935,370],{"class":46},[33,937,938],{"class":50}," \"command outputs matches regex\"",[33,940,376],{"class":50},[33,942,943,945,947],{"class":35,"line":421},[33,944,636],{"class":46},[33,946,639],{"class":50},[33,948,949],{"class":50}," \"2024-01-15\"\n",[33,951,952,954,956,958,960,963,966,969,972,975,977,979],{"class":35,"line":426},[33,953,308],{"class":117},[33,955,318],{"class":50},[33,957,582],{"class":117},[33,959,318],{"class":50},[33,961,962],{"class":227}," =~",[33,964,965],{"class":117}," ^[0-9]{",[33,967,968],{"class":68},"4",[33,970,971],{"class":117},"}-[0-9]{",[33,973,974],{"class":68},"2",[33,976,971],{"class":117},[33,978,974],{"class":68},[33,980,981],{"class":117},"}$ ]]\n",[33,983,984],{"class":35,"line":436},[33,985,283],{"class":117},[33,987,988],{"class":35,"line":466},[33,989,137],{"emptyLinePlaceholder":136},[33,991,992],{"class":35,"line":486},[33,993,994],{"class":39},"# ── ⚠️ `run` captures stdout AND stderr together ──\n",[33,996,997,999,1002],{"class":35,"line":491},[33,998,370],{"class":46},[33,1000,1001],{"class":50}," \"stderr is captured\"",[33,1003,376],{"class":50},[33,1005,1006,1008,1010,1012],{"class":35,"line":496},[33,1007,636],{"class":46},[33,1009,700],{"class":50},[33,1011,703],{"class":68},[33,1013,1014],{"class":50}," 'echo \"stdout\"; echo \"stderr\" >&2'\n",[33,1016,1017,1019,1021,1023,1025,1027,1029,1032,1034],{"class":35,"line":506},[33,1018,308],{"class":117},[33,1020,318],{"class":50},[33,1022,582],{"class":117},[33,1024,318],{"class":50},[33,1026,478],{"class":227},[33,1028,911],{"class":227},[33,1030,1031],{"class":117},"stdout",[33,1033,917],{"class":227},[33,1035,413],{"class":117},[33,1037,1038,1040,1042,1044,1046,1048,1050,1053,1055,1057],{"class":35,"line":523},[33,1039,308],{"class":117},[33,1041,318],{"class":50},[33,1043,582],{"class":117},[33,1045,318],{"class":50},[33,1047,478],{"class":227},[33,1049,911],{"class":227},[33,1051,1052],{"class":117},"stderr",[33,1054,917],{"class":227},[33,1056,920],{"class":117},[33,1058,1059],{"class":39},"# both captured (order may vary)\n",[33,1061,1062],{"class":35,"line":528},[33,1063,283],{"class":117},[33,1065,1066],{"class":35,"line":533},[33,1067,137],{"emptyLinePlaceholder":136},[33,1069,1070],{"class":35,"line":543},[33,1071,1072],{"class":39},"# ── Separate stderr (bats-core 1.5+) ──\n",[33,1074,1075,1077,1080],{"class":35,"line":567},[33,1076,370],{"class":46},[33,1078,1079],{"class":50}," \"separate stderr\"",[33,1081,376],{"class":50},[33,1083,1085,1087,1090,1092,1094],{"class":35,"line":1084},48,[33,1086,636],{"class":46},[33,1088,1089],{"class":68}," --separate-stderr",[33,1091,700],{"class":50},[33,1093,703],{"class":68},[33,1095,1096],{"class":50}," 'echo \"out\"; echo \"err\" >&2'\n",[33,1098,1100,1102,1104,1106,1108,1110,1113],{"class":35,"line":1099},49,[33,1101,308],{"class":117},[33,1103,318],{"class":50},[33,1105,582],{"class":117},[33,1107,318],{"class":50},[33,1109,478],{"class":227},[33,1111,1112],{"class":50}," \"out\"",[33,1114,413],{"class":117},[33,1116,1118,1120,1122,1125,1127,1129,1132],{"class":35,"line":1117},50,[33,1119,308],{"class":117},[33,1121,318],{"class":50},[33,1123,1124],{"class":117},"$stderr",[33,1126,318],{"class":50},[33,1128,478],{"class":227},[33,1130,1131],{"class":50}," \"err\"",[33,1133,413],{"class":117},[33,1135,1137],{"class":35,"line":1136},51,[33,1138,283],{"class":117},[15,1140,1142],{"id":1141},"testing-script-functions-source-and-test","Testing Script Functions (Source and Test)",[20,1144,1145],{"language":22},[24,1146,1148],{"className":26,"code":1147,"language":22,"meta":28,"style":28},"# ── lib\u002Fmath.sh: the library under test ──\nadd() {\n    echo \"$(( $1 + $2 ))\"\n}\n\nis_even() {\n    (( $1 % 2 == 0 ))\n}\n\nfactorial() {\n    local n=$1 result=1\n    for ((i = 2; i \u003C= n; i++)); do\n        ((result *= i))\n    done\n    echo \"$result\"\n}\n\nvalidate_email() {\n    [[ \"$1\" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$ ]]\n}\n\n# ── test\u002Fmath_test.bats: tests for lib\u002Fmath.sh ──\n#!\u002Fusr\u002Fbin\u002Fenv bats\n\n# Source the library (functions become available in the test shell)\nsource \"$(dirname \"$BATS_TEST_FILENAME\")\u002F..\u002Flib\u002Fmath.sh\"\n\n@test \"add returns the sum\" {\n    [[ \"$(add 2 3)\" == \"5\" ]]\n}\n\n@test \"add with negative numbers\" {\n    [[ \"$(add -5 -3)\" == \"-8\" ]]\n}\n\n@test \"add with zero\" {\n    [[ \"$(add 0 0)\" == \"0\" ]]\n}\n\n@test \"is_even returns 0 for even numbers\" {\n    run is_even 4\n    [[ \"$status\" -eq 0 ]]\n}\n\n@test \"is_even returns non-zero for odd numbers\" {\n    run is_even 3\n    [[ \"$status\" -ne 0 ]]\n}\n\n@test \"factorial of 0 is 1\" {\n    [[ \"$(factorial 0)\" == \"1\" ]]\n}\n\n@test \"factorial of 5 is 120\" {\n    [[ \"$(factorial 5)\" == \"120\" ]]\n}\n\n@test \"validate_email accepts valid email\" {\n    run validate_email \"user@example.com\"\n    [[ \"$status\" -eq 0 ]]\n}\n\n@test \"validate_email rejects missing @\" {\n    run validate_email \"userexample.com\"\n    [[ \"$status\" -ne 0 ]]\n}\n",[30,1149,1150,1155,1162,1181,1185,1189,1196,1217,1221,1225,1232,1252,1282,1293,1298,1308,1312,1316,1323,1344,1348,1352,1357,1361,1365,1370,1389,1393,1402,1424,1428,1432,1441,1464,1468,1472,1481,1502,1506,1510,1519,1529,1545,1549,1553,1562,1571,1588,1592,1596,1605,1624,1629,1634,1644,1664,1669,1674,1684,1695,1712,1717,1722,1732,1742,1759],{"__ignoreMap":28},[33,1151,1152],{"class":35,"line":36},[33,1153,1154],{"class":39},"# ── lib\u002Fmath.sh: the library under test ──\n",[33,1156,1157,1160],{"class":35,"line":43},[33,1158,1159],{"class":46},"add",[33,1161,212],{"class":117},[33,1163,1164,1166,1169,1172,1175,1178],{"class":35,"line":60},[33,1165,246],{"class":68},[33,1167,1168],{"class":50}," \"$(( ",[33,1170,1171],{"class":68},"$1",[33,1173,1174],{"class":50}," + ",[33,1176,1177],{"class":68},"$2",[33,1179,1180],{"class":50}," ))\"\n",[33,1182,1183],{"class":35,"line":78},[33,1184,283],{"class":117},[33,1186,1187],{"class":35,"line":91},[33,1188,137],{"emptyLinePlaceholder":136},[33,1190,1191,1194],{"class":35,"line":97},[33,1192,1193],{"class":46},"is_even",[33,1195,212],{"class":117},[33,1197,1198,1201,1204,1207,1210,1212,1214],{"class":35,"line":109},[33,1199,1200],{"class":117},"    (( ",[33,1202,1171],{"class":1203},"sCrzJ",[33,1205,1206],{"class":227}," %",[33,1208,1209],{"class":68}," 2",[33,1211,478],{"class":227},[33,1213,657],{"class":68},[33,1215,1216],{"class":117}," ))\n",[33,1218,1219],{"class":35,"line":133},[33,1220,283],{"class":117},[33,1222,1223],{"class":35,"line":140},[33,1224,137],{"emptyLinePlaceholder":136},[33,1226,1227,1230],{"class":35,"line":146},[33,1228,1229],{"class":46},"factorial",[33,1231,212],{"class":117},[33,1233,1234,1237,1240,1242,1244,1247,1249],{"class":35,"line":159},[33,1235,1236],{"class":227},"    local",[33,1238,1239],{"class":117}," n",[33,1241,228],{"class":227},[33,1243,1171],{"class":1203},[33,1245,1246],{"class":117}," result",[33,1248,228],{"class":227},[33,1250,1251],{"class":68},"1\n",[33,1253,1254,1257,1260,1262,1264,1267,1270,1273,1276,1279],{"class":35,"line":172},[33,1255,1256],{"class":227},"    for",[33,1258,1259],{"class":117}," ((i ",[33,1261,228],{"class":227},[33,1263,1209],{"class":68},[33,1265,1266],{"class":117},"; i ",[33,1268,1269],{"class":227},"\u003C=",[33,1271,1272],{"class":117}," n; i",[33,1274,1275],{"class":227},"++",[33,1277,1278],{"class":117},")); ",[33,1280,1281],{"class":227},"do\n",[33,1283,1284,1287,1290],{"class":35,"line":177},[33,1285,1286],{"class":117},"        ((result ",[33,1288,1289],{"class":227},"*=",[33,1291,1292],{"class":117}," i))\n",[33,1294,1295],{"class":35,"line":183},[33,1296,1297],{"class":227},"    done\n",[33,1299,1300,1302,1304,1306],{"class":35,"line":189},[33,1301,246],{"class":68},[33,1303,124],{"class":50},[33,1305,473],{"class":117},[33,1307,348],{"class":50},[33,1309,1310],{"class":35,"line":195},[33,1311,283],{"class":117},[33,1313,1314],{"class":35,"line":200},[33,1315,137],{"emptyLinePlaceholder":136},[33,1317,1318,1321],{"class":35,"line":206},[33,1319,1320],{"class":46},"validate_email",[33,1322,212],{"class":117},[33,1324,1325,1327,1329,1331,1333,1335,1338,1341],{"class":35,"line":215},[33,1326,308],{"class":117},[33,1328,318],{"class":50},[33,1330,1171],{"class":68},[33,1332,318],{"class":50},[33,1334,962],{"class":227},[33,1336,1337],{"class":117}," ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+",[33,1339,1340],{"class":68},"\\.",[33,1342,1343],{"class":117},"[a-zA-Z]{2,}$ ]]\n",[33,1345,1346],{"class":35,"line":221},[33,1347,283],{"class":117},[33,1349,1350],{"class":35,"line":243},[33,1351,137],{"emptyLinePlaceholder":136},[33,1353,1354],{"class":35,"line":263},[33,1355,1356],{"class":39},"# ── test\u002Fmath_test.bats: tests for lib\u002Fmath.sh ──\n",[33,1358,1359],{"class":35,"line":280},[33,1360,192],{"class":39},[33,1362,1363],{"class":35,"line":286},[33,1364,137],{"emptyLinePlaceholder":136},[33,1366,1367],{"class":35,"line":291},[33,1368,1369],{"class":39},"# Source the library (functions become available in the test shell)\n",[33,1371,1372,1375,1378,1381,1383,1386],{"class":35,"line":299},[33,1373,1374],{"class":68},"source",[33,1376,1377],{"class":50}," \"$(",[33,1379,1380],{"class":46},"dirname",[33,1382,124],{"class":50},[33,1384,1385],{"class":117},"$BATS_TEST_FILENAME",[33,1387,1388],{"class":50},"\")\u002F..\u002Flib\u002Fmath.sh\"\n",[33,1390,1391],{"class":35,"line":305},[33,1392,137],{"emptyLinePlaceholder":136},[33,1394,1395,1397,1400],{"class":35,"line":351},[33,1396,370],{"class":46},[33,1398,1399],{"class":50}," \"add returns the sum\"",[33,1401,376],{"class":50},[33,1403,1404,1406,1408,1410,1412,1414,1417,1419,1422],{"class":35,"line":356},[33,1405,308],{"class":117},[33,1407,548],{"class":50},[33,1409,1159],{"class":46},[33,1411,1209],{"class":68},[33,1413,800],{"class":68},[33,1415,1416],{"class":50},")\"",[33,1418,478],{"class":227},[33,1420,1421],{"class":50}," \"5\"",[33,1423,413],{"class":117},[33,1425,1426],{"class":35,"line":361},[33,1427,283],{"class":117},[33,1429,1430],{"class":35,"line":367},[33,1431,137],{"emptyLinePlaceholder":136},[33,1433,1434,1436,1439],{"class":35,"line":379},[33,1435,370],{"class":46},[33,1437,1438],{"class":50}," \"add with negative numbers\"",[33,1440,376],{"class":50},[33,1442,1443,1445,1447,1449,1452,1455,1457,1459,1462],{"class":35,"line":390},[33,1444,308],{"class":117},[33,1446,548],{"class":50},[33,1448,1159],{"class":46},[33,1450,1451],{"class":68}," -5",[33,1453,1454],{"class":68}," -3",[33,1456,1416],{"class":50},[33,1458,478],{"class":227},[33,1460,1461],{"class":50}," \"-8\"",[33,1463,413],{"class":117},[33,1465,1466],{"class":35,"line":416},[33,1467,283],{"class":117},[33,1469,1470],{"class":35,"line":421},[33,1471,137],{"emptyLinePlaceholder":136},[33,1473,1474,1476,1479],{"class":35,"line":426},[33,1475,370],{"class":46},[33,1477,1478],{"class":50}," \"add with zero\"",[33,1480,376],{"class":50},[33,1482,1483,1485,1487,1489,1491,1493,1495,1497,1500],{"class":35,"line":436},[33,1484,308],{"class":117},[33,1486,548],{"class":50},[33,1488,1159],{"class":46},[33,1490,657],{"class":68},[33,1492,657],{"class":68},[33,1494,1416],{"class":50},[33,1496,478],{"class":227},[33,1498,1499],{"class":50}," \"0\"",[33,1501,413],{"class":117},[33,1503,1504],{"class":35,"line":466},[33,1505,283],{"class":117},[33,1507,1508],{"class":35,"line":486},[33,1509,137],{"emptyLinePlaceholder":136},[33,1511,1512,1514,1517],{"class":35,"line":491},[33,1513,370],{"class":46},[33,1515,1516],{"class":50}," \"is_even returns 0 for even numbers\"",[33,1518,376],{"class":50},[33,1520,1521,1523,1526],{"class":35,"line":496},[33,1522,636],{"class":46},[33,1524,1525],{"class":50}," is_even",[33,1527,1528],{"class":68}," 4\n",[33,1530,1531,1533,1535,1537,1539,1541,1543],{"class":35,"line":506},[33,1532,308],{"class":117},[33,1534,318],{"class":50},[33,1536,579],{"class":117},[33,1538,318],{"class":50},[33,1540,407],{"class":227},[33,1542,657],{"class":68},[33,1544,413],{"class":117},[33,1546,1547],{"class":35,"line":523},[33,1548,283],{"class":117},[33,1550,1551],{"class":35,"line":528},[33,1552,137],{"emptyLinePlaceholder":136},[33,1554,1555,1557,1560],{"class":35,"line":533},[33,1556,370],{"class":46},[33,1558,1559],{"class":50}," \"is_even returns non-zero for odd numbers\"",[33,1561,376],{"class":50},[33,1563,1564,1566,1568],{"class":35,"line":543},[33,1565,636],{"class":46},[33,1567,1525],{"class":50},[33,1569,1570],{"class":68}," 3\n",[33,1572,1573,1575,1577,1579,1581,1584,1586],{"class":35,"line":567},[33,1574,308],{"class":117},[33,1576,318],{"class":50},[33,1578,579],{"class":117},[33,1580,318],{"class":50},[33,1582,1583],{"class":227}," -ne",[33,1585,657],{"class":68},[33,1587,413],{"class":117},[33,1589,1590],{"class":35,"line":1084},[33,1591,283],{"class":117},[33,1593,1594],{"class":35,"line":1099},[33,1595,137],{"emptyLinePlaceholder":136},[33,1597,1598,1600,1603],{"class":35,"line":1117},[33,1599,370],{"class":46},[33,1601,1602],{"class":50}," \"factorial of 0 is 1\"",[33,1604,376],{"class":50},[33,1606,1607,1609,1611,1613,1615,1617,1619,1622],{"class":35,"line":1136},[33,1608,308],{"class":117},[33,1610,548],{"class":50},[33,1612,1229],{"class":46},[33,1614,657],{"class":68},[33,1616,1416],{"class":50},[33,1618,478],{"class":227},[33,1620,1621],{"class":50}," \"1\"",[33,1623,413],{"class":117},[33,1625,1627],{"class":35,"line":1626},52,[33,1628,283],{"class":117},[33,1630,1632],{"class":35,"line":1631},53,[33,1633,137],{"emptyLinePlaceholder":136},[33,1635,1637,1639,1642],{"class":35,"line":1636},54,[33,1638,370],{"class":46},[33,1640,1641],{"class":50}," \"factorial of 5 is 120\"",[33,1643,376],{"class":50},[33,1645,1647,1649,1651,1653,1655,1657,1659,1662],{"class":35,"line":1646},55,[33,1648,308],{"class":117},[33,1650,548],{"class":50},[33,1652,1229],{"class":46},[33,1654,410],{"class":68},[33,1656,1416],{"class":50},[33,1658,478],{"class":227},[33,1660,1661],{"class":50}," \"120\"",[33,1663,413],{"class":117},[33,1665,1667],{"class":35,"line":1666},56,[33,1668,283],{"class":117},[33,1670,1672],{"class":35,"line":1671},57,[33,1673,137],{"emptyLinePlaceholder":136},[33,1675,1677,1679,1682],{"class":35,"line":1676},58,[33,1678,370],{"class":46},[33,1680,1681],{"class":50}," \"validate_email accepts valid email\"",[33,1683,376],{"class":50},[33,1685,1687,1689,1692],{"class":35,"line":1686},59,[33,1688,636],{"class":46},[33,1690,1691],{"class":50}," validate_email",[33,1693,1694],{"class":50}," \"user@example.com\"\n",[33,1696,1698,1700,1702,1704,1706,1708,1710],{"class":35,"line":1697},60,[33,1699,308],{"class":117},[33,1701,318],{"class":50},[33,1703,579],{"class":117},[33,1705,318],{"class":50},[33,1707,407],{"class":227},[33,1709,657],{"class":68},[33,1711,413],{"class":117},[33,1713,1715],{"class":35,"line":1714},61,[33,1716,283],{"class":117},[33,1718,1720],{"class":35,"line":1719},62,[33,1721,137],{"emptyLinePlaceholder":136},[33,1723,1725,1727,1730],{"class":35,"line":1724},63,[33,1726,370],{"class":46},[33,1728,1729],{"class":50}," \"validate_email rejects missing @\"",[33,1731,376],{"class":50},[33,1733,1735,1737,1739],{"class":35,"line":1734},64,[33,1736,636],{"class":46},[33,1738,1691],{"class":50},[33,1740,1741],{"class":50}," \"userexample.com\"\n",[33,1743,1745,1747,1749,1751,1753,1755,1757],{"class":35,"line":1744},65,[33,1746,308],{"class":117},[33,1748,318],{"class":50},[33,1750,579],{"class":117},[33,1752,318],{"class":50},[33,1754,1583],{"class":227},[33,1756,657],{"class":68},[33,1758,413],{"class":117},[33,1760,1762],{"class":35,"line":1761},66,[33,1763,283],{"class":117},[15,1765,1767],{"id":1766},"bats-assert-readable-assertions","bats-assert: Readable Assertions",[20,1769,1770],{"language":22},[24,1771,1773],{"className":26,"code":1772,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bats\n\n# Load bats-support and bats-assert (for assertion helpers)\nload '..\u002Ftest_helper\u002Fbats-support\u002Fload.bash'\nload '..\u002Ftest_helper\u002Fbats-assert\u002Fload.bash'\n\n@test \"assert_success\" {\n    run echo \"hello\"\n    assert_success   # $status is 0\n}\n\n@test \"assert_failure\" {\n    run false\n    assert_failure   # $status is non-zero\n}\n\n@test \"assert_failure with specific code\" {\n    run bash -c 'exit 42'\n    assert_failure 42   # $status is 42\n}\n\n@test \"assert_output exact match\" {\n    run echo \"hello\"\n    assert_output \"hello\"\n}\n\n@test \"assert_output substring match\" {\n    run echo \"hello world\"\n    assert_output --partial \"world\"\n}\n\n@test \"assert_output regex match\" {\n    run echo \"2024-01-15\"\n    assert_output --regexp \"[0-9]{4}-[0-9]{2}-[0-9]{2}\"\n}\n\n@test \"assert_equal\" {\n    assert_equal \"hello\" \"hello\"\n    assert_equal \"$(add 2 3)\" \"5\"\n}\n\n@test \"refute_output (negate)\" {\n    run echo \"hello\"\n    refute_output \"world\"   # output is NOT \"world\"\n}\n\n@test \"assert_line (check specific line)\" {\n    run printf '%s\\n' \"line1\" \"line2\" \"line3\"\n    assert_line --index 0 \"line1\"\n    assert_line --index 1 \"line2\"\n    assert_line --partial \"line\"\n}\n",[30,1774,1775,1779,1783,1788,1796,1803,1807,1816,1824,1832,1836,1840,1849,1856,1864,1868,1872,1881,1891,1900,1904,1908,1917,1925,1932,1936,1940,1949,1958,1968,1972,1976,1985,1993,2003,2007,2011,2020,2029,2046,2050,2054,2063,2071,2082,2086,2090,2099,2113,2126,2138,2147],{"__ignoreMap":28},[33,1776,1777],{"class":35,"line":36},[33,1778,192],{"class":39},[33,1780,1781],{"class":35,"line":43},[33,1782,137],{"emptyLinePlaceholder":136},[33,1784,1785],{"class":35,"line":60},[33,1786,1787],{"class":39},"# Load bats-support and bats-assert (for assertion helpers)\n",[33,1789,1790,1793],{"class":35,"line":78},[33,1791,1792],{"class":46},"load",[33,1794,1795],{"class":50}," '..\u002Ftest_helper\u002Fbats-support\u002Fload.bash'\n",[33,1797,1798,1800],{"class":35,"line":91},[33,1799,1792],{"class":46},[33,1801,1802],{"class":50}," '..\u002Ftest_helper\u002Fbats-assert\u002Fload.bash'\n",[33,1804,1805],{"class":35,"line":97},[33,1806,137],{"emptyLinePlaceholder":136},[33,1808,1809,1811,1814],{"class":35,"line":109},[33,1810,370],{"class":46},[33,1812,1813],{"class":50}," \"assert_success\"",[33,1815,376],{"class":50},[33,1817,1818,1820,1822],{"class":35,"line":133},[33,1819,636],{"class":46},[33,1821,639],{"class":50},[33,1823,642],{"class":50},[33,1825,1826,1829],{"class":35,"line":140},[33,1827,1828],{"class":46},"    assert_success",[33,1830,1831],{"class":39},"   # $status is 0\n",[33,1833,1834],{"class":35,"line":146},[33,1835,283],{"class":117},[33,1837,1838],{"class":35,"line":159},[33,1839,137],{"emptyLinePlaceholder":136},[33,1841,1842,1844,1847],{"class":35,"line":172},[33,1843,370],{"class":46},[33,1845,1846],{"class":50}," \"assert_failure\"",[33,1848,376],{"class":50},[33,1850,1851,1853],{"class":35,"line":177},[33,1852,636],{"class":46},[33,1854,1855],{"class":68}," false\n",[33,1857,1858,1861],{"class":35,"line":183},[33,1859,1860],{"class":46},"    assert_failure",[33,1862,1863],{"class":39},"   # $status is non-zero\n",[33,1865,1866],{"class":35,"line":189},[33,1867,283],{"class":117},[33,1869,1870],{"class":35,"line":195},[33,1871,137],{"emptyLinePlaceholder":136},[33,1873,1874,1876,1879],{"class":35,"line":200},[33,1875,370],{"class":46},[33,1877,1878],{"class":50}," \"assert_failure with specific code\"",[33,1880,376],{"class":50},[33,1882,1883,1885,1887,1889],{"class":35,"line":206},[33,1884,636],{"class":46},[33,1886,700],{"class":50},[33,1888,703],{"class":68},[33,1890,706],{"class":50},[33,1892,1893,1895,1897],{"class":35,"line":215},[33,1894,1860],{"class":46},[33,1896,721],{"class":68},[33,1898,1899],{"class":39},"   # $status is 42\n",[33,1901,1902],{"class":35,"line":221},[33,1903,283],{"class":117},[33,1905,1906],{"class":35,"line":243},[33,1907,137],{"emptyLinePlaceholder":136},[33,1909,1910,1912,1915],{"class":35,"line":263},[33,1911,370],{"class":46},[33,1913,1914],{"class":50}," \"assert_output exact match\"",[33,1916,376],{"class":50},[33,1918,1919,1921,1923],{"class":35,"line":280},[33,1920,636],{"class":46},[33,1922,639],{"class":50},[33,1924,642],{"class":50},[33,1926,1927,1930],{"class":35,"line":286},[33,1928,1929],{"class":46},"    assert_output",[33,1931,642],{"class":50},[33,1933,1934],{"class":35,"line":291},[33,1935,283],{"class":117},[33,1937,1938],{"class":35,"line":299},[33,1939,137],{"emptyLinePlaceholder":136},[33,1941,1942,1944,1947],{"class":35,"line":305},[33,1943,370],{"class":46},[33,1945,1946],{"class":50}," \"assert_output substring match\"",[33,1948,376],{"class":50},[33,1950,1951,1953,1955],{"class":35,"line":351},[33,1952,636],{"class":46},[33,1954,639],{"class":50},[33,1956,1957],{"class":50}," \"hello world\"\n",[33,1959,1960,1962,1965],{"class":35,"line":356},[33,1961,1929],{"class":46},[33,1963,1964],{"class":68}," --partial",[33,1966,1967],{"class":50}," \"world\"\n",[33,1969,1970],{"class":35,"line":361},[33,1971,283],{"class":117},[33,1973,1974],{"class":35,"line":367},[33,1975,137],{"emptyLinePlaceholder":136},[33,1977,1978,1980,1983],{"class":35,"line":379},[33,1979,370],{"class":46},[33,1981,1982],{"class":50}," \"assert_output regex match\"",[33,1984,376],{"class":50},[33,1986,1987,1989,1991],{"class":35,"line":390},[33,1988,636],{"class":46},[33,1990,639],{"class":50},[33,1992,949],{"class":50},[33,1994,1995,1997,2000],{"class":35,"line":416},[33,1996,1929],{"class":46},[33,1998,1999],{"class":68}," --regexp",[33,2001,2002],{"class":50}," \"[0-9]{4}-[0-9]{2}-[0-9]{2}\"\n",[33,2004,2005],{"class":35,"line":421},[33,2006,283],{"class":117},[33,2008,2009],{"class":35,"line":426},[33,2010,137],{"emptyLinePlaceholder":136},[33,2012,2013,2015,2018],{"class":35,"line":436},[33,2014,370],{"class":46},[33,2016,2017],{"class":50}," \"assert_equal\"",[33,2019,376],{"class":50},[33,2021,2022,2025,2027],{"class":35,"line":466},[33,2023,2024],{"class":46},"    assert_equal",[33,2026,674],{"class":50},[33,2028,642],{"class":50},[33,2030,2031,2033,2035,2037,2039,2041,2043],{"class":35,"line":486},[33,2032,2024],{"class":46},[33,2034,1377],{"class":50},[33,2036,1159],{"class":46},[33,2038,1209],{"class":68},[33,2040,800],{"class":68},[33,2042,1416],{"class":50},[33,2044,2045],{"class":50}," \"5\"\n",[33,2047,2048],{"class":35,"line":491},[33,2049,283],{"class":117},[33,2051,2052],{"class":35,"line":496},[33,2053,137],{"emptyLinePlaceholder":136},[33,2055,2056,2058,2061],{"class":35,"line":506},[33,2057,370],{"class":46},[33,2059,2060],{"class":50}," \"refute_output (negate)\"",[33,2062,376],{"class":50},[33,2064,2065,2067,2069],{"class":35,"line":523},[33,2066,636],{"class":46},[33,2068,639],{"class":50},[33,2070,642],{"class":50},[33,2072,2073,2076,2079],{"class":35,"line":528},[33,2074,2075],{"class":46},"    refute_output",[33,2077,2078],{"class":50}," \"world\"",[33,2080,2081],{"class":39},"   # output is NOT \"world\"\n",[33,2083,2084],{"class":35,"line":533},[33,2085,283],{"class":117},[33,2087,2088],{"class":35,"line":543},[33,2089,137],{"emptyLinePlaceholder":136},[33,2091,2092,2094,2097],{"class":35,"line":567},[33,2093,370],{"class":46},[33,2095,2096],{"class":50}," \"assert_line (check specific line)\"",[33,2098,376],{"class":50},[33,2100,2101,2103,2105,2107,2109,2111],{"class":35,"line":1084},[33,2102,636],{"class":46},[33,2104,747],{"class":50},[33,2106,750],{"class":50},[33,2108,753],{"class":50},[33,2110,756],{"class":50},[33,2112,759],{"class":50},[33,2114,2115,2118,2121,2123],{"class":35,"line":1099},[33,2116,2117],{"class":46},"    assert_line",[33,2119,2120],{"class":68}," --index",[33,2122,657],{"class":68},[33,2124,2125],{"class":50}," \"line1\"\n",[33,2127,2128,2130,2132,2135],{"class":35,"line":1117},[33,2129,2117],{"class":46},[33,2131,2120],{"class":68},[33,2133,2134],{"class":68}," 1",[33,2136,2137],{"class":50}," \"line2\"\n",[33,2139,2140,2142,2144],{"class":35,"line":1136},[33,2141,2117],{"class":46},[33,2143,1964],{"class":68},[33,2145,2146],{"class":50}," \"line\"\n",[33,2148,2149],{"class":35,"line":1626},[33,2150,283],{"class":117},[15,2152,2154],{"id":2153},"mocking-override-commands-with-functions","Mocking: Override Commands with Functions",[20,2156,2157],{"language":22},[24,2158,2160],{"className":26,"code":2159,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bats\n\n# ── Mock: override a command with a function ──\n# The function has the same name as the command.\n# `export -f` makes it available in subshells (where `run` executes).\n\nsetup() {\n    # Create mock functions for this test\n    mock_curl() {\n        echo '{\"status\": \"ok\"}'\n        return 0\n    }\n    export -f mock_curl\n}\n\n@test \"script handles successful API call\" {\n    # Create a mock for `curl`\n    curl() {\n        echo '{\"status\": \"ok\"}'\n        return 0\n    }\n    export -f curl\n\n    # The script calls curl — our mock returns success\n    run .\u002Fmy_script.sh --api-check\n    assert_success\n    assert_output --partial \"API is healthy\"\n}\n\n@test \"script handles failed API call\" {\n    # Mock curl to fail\n    curl() {\n        echo \"connection refused\" >&2\n        return 7   # curl exit 7 = connection refused\n    }\n    export -f curl\n\n    run .\u002Fmy_script.sh --api-check\n    assert_failure\n    assert_output --partial \"API is down\"\n}\n\n@test \"mock with argument capture\" {\n    # Capture arguments passed to the mock\n    mock_git() {\n        echo \"MOCK: git $*\" >> \"$BATS_TEST_TMPDIR\u002Fgit_calls.log\"\n        return 0\n    }\n    export -f mock_git\n    export BATS_TEST_TMPDIR\n\n    # Override git\n    git() { mock_git \"$@\"; }\n    export -f git\n\n    run .\u002Fdeploy_script.sh\n    assert_success\n\n    # Verify git was called with expected args\n    [[ -f \"$BATS_TEST_TMPDIR\u002Fgit_calls.log\" ]]\n    grep -q \"git push\" \"$BATS_TEST_TMPDIR\u002Fgit_calls.log\"\n}\n\n# ── Restore in teardown ──\nteardown() {\n    unset -f curl git mock_curl mock_git 2>\u002Fdev\u002Fnull\n}\n",[30,2161,2162,2166,2170,2175,2180,2185,2189,2195,2200,2207,2215,2223,2228,2239,2243,2247,2256,2261,2268,2274,2280,2284,2293,2297,2302,2312,2317,2326,2330,2334,2343,2348,2354,2364,2374,2378,2386,2390,2398,2403,2412,2416,2420,2429,2434,2441,2464,2470,2474,2483,2490,2494,2499,2520,2529,2533,2540,2544,2548,2553,2568,2585,2589,2593,2598,2604,2629],{"__ignoreMap":28},[33,2163,2164],{"class":35,"line":36},[33,2165,192],{"class":39},[33,2167,2168],{"class":35,"line":43},[33,2169,137],{"emptyLinePlaceholder":136},[33,2171,2172],{"class":35,"line":60},[33,2173,2174],{"class":39},"# ── Mock: override a command with a function ──\n",[33,2176,2177],{"class":35,"line":78},[33,2178,2179],{"class":39},"# The function has the same name as the command.\n",[33,2181,2182],{"class":35,"line":91},[33,2183,2184],{"class":39},"# `export -f` makes it available in subshells (where `run` executes).\n",[33,2186,2187],{"class":35,"line":97},[33,2188,137],{"emptyLinePlaceholder":136},[33,2190,2191,2193],{"class":35,"line":109},[33,2192,209],{"class":46},[33,2194,212],{"class":117},[33,2196,2197],{"class":35,"line":133},[33,2198,2199],{"class":39},"    # Create mock functions for this test\n",[33,2201,2202,2205],{"class":35,"line":140},[33,2203,2204],{"class":46},"    mock_curl",[33,2206,212],{"class":117},[33,2208,2209,2212],{"class":35,"line":146},[33,2210,2211],{"class":68},"        echo",[33,2213,2214],{"class":50}," '{\"status\": \"ok\"}'\n",[33,2216,2217,2220],{"class":35,"line":159},[33,2218,2219],{"class":227},"        return",[33,2221,2222],{"class":68}," 0\n",[33,2224,2225],{"class":35,"line":172},[33,2226,2227],{"class":117},"    }\n",[33,2229,2230,2233,2236],{"class":35,"line":177},[33,2231,2232],{"class":227},"    export",[33,2234,2235],{"class":68}," -f",[33,2237,2238],{"class":117}," mock_curl\n",[33,2240,2241],{"class":35,"line":183},[33,2242,283],{"class":117},[33,2244,2245],{"class":35,"line":189},[33,2246,137],{"emptyLinePlaceholder":136},[33,2248,2249,2251,2254],{"class":35,"line":195},[33,2250,370],{"class":46},[33,2252,2253],{"class":50}," \"script handles successful API call\"",[33,2255,376],{"class":50},[33,2257,2258],{"class":35,"line":200},[33,2259,2260],{"class":39},"    # Create a mock for `curl`\n",[33,2262,2263,2266],{"class":35,"line":206},[33,2264,2265],{"class":46},"    curl",[33,2267,212],{"class":117},[33,2269,2270,2272],{"class":35,"line":215},[33,2271,2211],{"class":68},[33,2273,2214],{"class":50},[33,2275,2276,2278],{"class":35,"line":221},[33,2277,2219],{"class":227},[33,2279,2222],{"class":68},[33,2281,2282],{"class":35,"line":243},[33,2283,2227],{"class":117},[33,2285,2286,2288,2290],{"class":35,"line":263},[33,2287,2232],{"class":227},[33,2289,2235],{"class":68},[33,2291,2292],{"class":117}," curl\n",[33,2294,2295],{"class":35,"line":280},[33,2296,137],{"emptyLinePlaceholder":136},[33,2298,2299],{"class":35,"line":286},[33,2300,2301],{"class":39},"    # The script calls curl — our mock returns success\n",[33,2303,2304,2306,2309],{"class":35,"line":291},[33,2305,636],{"class":46},[33,2307,2308],{"class":50}," .\u002Fmy_script.sh",[33,2310,2311],{"class":68}," --api-check\n",[33,2313,2314],{"class":35,"line":299},[33,2315,2316],{"class":46},"    assert_success\n",[33,2318,2319,2321,2323],{"class":35,"line":305},[33,2320,1929],{"class":46},[33,2322,1964],{"class":68},[33,2324,2325],{"class":50}," \"API is healthy\"\n",[33,2327,2328],{"class":35,"line":351},[33,2329,283],{"class":117},[33,2331,2332],{"class":35,"line":356},[33,2333,137],{"emptyLinePlaceholder":136},[33,2335,2336,2338,2341],{"class":35,"line":361},[33,2337,370],{"class":46},[33,2339,2340],{"class":50}," \"script handles failed API call\"",[33,2342,376],{"class":50},[33,2344,2345],{"class":35,"line":367},[33,2346,2347],{"class":39},"    # Mock curl to fail\n",[33,2349,2350,2352],{"class":35,"line":379},[33,2351,2265],{"class":46},[33,2353,212],{"class":117},[33,2355,2356,2358,2361],{"class":35,"line":390},[33,2357,2211],{"class":68},[33,2359,2360],{"class":50}," \"connection refused\"",[33,2362,2363],{"class":227}," >&2\n",[33,2365,2366,2368,2371],{"class":35,"line":416},[33,2367,2219],{"class":227},[33,2369,2370],{"class":68}," 7",[33,2372,2373],{"class":39},"   # curl exit 7 = connection refused\n",[33,2375,2376],{"class":35,"line":421},[33,2377,2227],{"class":117},[33,2379,2380,2382,2384],{"class":35,"line":426},[33,2381,2232],{"class":227},[33,2383,2235],{"class":68},[33,2385,2292],{"class":117},[33,2387,2388],{"class":35,"line":436},[33,2389,137],{"emptyLinePlaceholder":136},[33,2391,2392,2394,2396],{"class":35,"line":466},[33,2393,636],{"class":46},[33,2395,2308],{"class":50},[33,2397,2311],{"class":68},[33,2399,2400],{"class":35,"line":486},[33,2401,2402],{"class":46},"    assert_failure\n",[33,2404,2405,2407,2409],{"class":35,"line":491},[33,2406,1929],{"class":46},[33,2408,1964],{"class":68},[33,2410,2411],{"class":50}," \"API is down\"\n",[33,2413,2414],{"class":35,"line":496},[33,2415,283],{"class":117},[33,2417,2418],{"class":35,"line":506},[33,2419,137],{"emptyLinePlaceholder":136},[33,2421,2422,2424,2427],{"class":35,"line":523},[33,2423,370],{"class":46},[33,2425,2426],{"class":50}," \"mock with argument capture\"",[33,2428,376],{"class":50},[33,2430,2431],{"class":35,"line":528},[33,2432,2433],{"class":39},"    # Capture arguments passed to the mock\n",[33,2435,2436,2439],{"class":35,"line":533},[33,2437,2438],{"class":46},"    mock_git",[33,2440,212],{"class":117},[33,2442,2443,2445,2448,2451,2453,2456,2458,2461],{"class":35,"line":543},[33,2444,2211],{"class":68},[33,2446,2447],{"class":50}," \"MOCK: git ",[33,2449,2450],{"class":68},"$*",[33,2452,318],{"class":50},[33,2454,2455],{"class":227}," >>",[33,2457,124],{"class":50},[33,2459,2460],{"class":117},"$BATS_TEST_TMPDIR",[33,2462,2463],{"class":50},"\u002Fgit_calls.log\"\n",[33,2465,2466,2468],{"class":35,"line":567},[33,2467,2219],{"class":227},[33,2469,2222],{"class":68},[33,2471,2472],{"class":35,"line":1084},[33,2473,2227],{"class":117},[33,2475,2476,2478,2480],{"class":35,"line":1099},[33,2477,2232],{"class":227},[33,2479,2235],{"class":68},[33,2481,2482],{"class":117}," mock_git\n",[33,2484,2485,2487],{"class":35,"line":1117},[33,2486,2232],{"class":227},[33,2488,2489],{"class":117}," BATS_TEST_TMPDIR\n",[33,2491,2492],{"class":35,"line":1136},[33,2493,137],{"emptyLinePlaceholder":136},[33,2495,2496],{"class":35,"line":1626},[33,2497,2498],{"class":39},"    # Override git\n",[33,2500,2501,2504,2507,2510,2512,2515,2517],{"class":35,"line":1631},[33,2502,2503],{"class":46},"    git",[33,2505,2506],{"class":117},"() { ",[33,2508,2509],{"class":46},"mock_git",[33,2511,124],{"class":50},[33,2513,2514],{"class":68},"$@",[33,2516,318],{"class":50},[33,2518,2519],{"class":117},"; }\n",[33,2521,2522,2524,2526],{"class":35,"line":1636},[33,2523,2232],{"class":227},[33,2525,2235],{"class":68},[33,2527,2528],{"class":117}," git\n",[33,2530,2531],{"class":35,"line":1646},[33,2532,137],{"emptyLinePlaceholder":136},[33,2534,2535,2537],{"class":35,"line":1666},[33,2536,636],{"class":46},[33,2538,2539],{"class":50}," .\u002Fdeploy_script.sh\n",[33,2541,2542],{"class":35,"line":1671},[33,2543,2316],{"class":46},[33,2545,2546],{"class":35,"line":1676},[33,2547,137],{"emptyLinePlaceholder":136},[33,2549,2550],{"class":35,"line":1686},[33,2551,2552],{"class":39},"    # Verify git was called with expected args\n",[33,2554,2555,2557,2559,2561,2563,2566],{"class":35,"line":1697},[33,2556,308],{"class":117},[33,2558,511],{"class":227},[33,2560,124],{"class":50},[33,2562,2460],{"class":117},[33,2564,2565],{"class":50},"\u002Fgit_calls.log\"",[33,2567,413],{"class":117},[33,2569,2570,2573,2576,2579,2581,2583],{"class":35,"line":1714},[33,2571,2572],{"class":46},"    grep",[33,2574,2575],{"class":68}," -q",[33,2577,2578],{"class":50}," \"git push\"",[33,2580,124],{"class":50},[33,2582,2460],{"class":117},[33,2584,2463],{"class":50},[33,2586,2587],{"class":35,"line":1719},[33,2588,283],{"class":117},[33,2590,2591],{"class":35,"line":1724},[33,2592,137],{"emptyLinePlaceholder":136},[33,2594,2595],{"class":35,"line":1734},[33,2596,2597],{"class":39},"# ── Restore in teardown ──\n",[33,2599,2600,2602],{"class":35,"line":1744},[33,2601,294],{"class":46},[33,2603,212],{"class":117},[33,2605,2606,2609,2611,2614,2617,2620,2623,2626],{"class":35,"line":1761},[33,2607,2608],{"class":68},"    unset",[33,2610,2235],{"class":68},[33,2612,2613],{"class":50}," curl",[33,2615,2616],{"class":50}," git",[33,2618,2619],{"class":50}," mock_curl",[33,2621,2622],{"class":50}," mock_git",[33,2624,2625],{"class":227}," 2>",[33,2627,2628],{"class":50},"\u002Fdev\u002Fnull\n",[33,2630,2632],{"class":35,"line":2631},67,[33,2633,283],{"class":117},[15,2635,2637],{"id":2636},"parameterized-tests","Parameterized Tests",[20,2639,2640],{"language":22},[24,2641,2643],{"className":26,"code":2642,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bats\n\nsource \"..\u002Flib\u002Fmath.sh\"\n\n# ── bats doesn't have built-in parameterized tests ──\n# But you can loop inside a test or generate tests with a helper.\n\n# ── Method 1: loop inside a test ──\n@test \"is_even for multiple values\" {\n    for n in 2 4 6 8 10; do\n        run is_even \"$n\"\n        [[ \"$status\" -eq 0 ]] || fail \"is_even $n should be even\"\n    done\n    for n in 1 3 5 7 9; do\n        run is_even \"$n\"\n        [[ \"$status\" -ne 0 ]] || fail \"is_even $n should be odd\"\n    done\n}\n\n# ── Method 2: table-driven with data array ──\n@test \"add table-driven\" {\n    # Format: \"a b expected\"\n    local data=(\n        \"1 2 3\"\n        \"0 0 0\"\n        \"-1 -1 -2\"\n        \"100 200 300\"\n    )\n    for row in \"${data[@]}\"; do\n        read -r a b expected \u003C\u003C\u003C \"$row\"\n        local actual\n        actual=$(add \"$a\" \"$b\")\n        [[ \"$actual\" == \"$expected\" ]] || fail \"add $a $b = $actual (expected $expected)\"\n    done\n}\n\n# ── Method 3: generate tests with bats-loop or a generator ──\n# bats-loop: https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-loop\n# Or generate .bats files from a template (for separate test per case)\n",[30,2644,2645,2649,2653,2660,2664,2669,2674,2678,2683,2692,2721,2735,2767,2771,2794,2806,2833,2837,2841,2845,2850,2859,2864,2876,2881,2886,2891,2896,2901,2926,2953,2961,2988,3035,3039,3043,3047,3052,3057],{"__ignoreMap":28},[33,2646,2647],{"class":35,"line":36},[33,2648,192],{"class":39},[33,2650,2651],{"class":35,"line":43},[33,2652,137],{"emptyLinePlaceholder":136},[33,2654,2655,2657],{"class":35,"line":60},[33,2656,1374],{"class":68},[33,2658,2659],{"class":50}," \"..\u002Flib\u002Fmath.sh\"\n",[33,2661,2662],{"class":35,"line":78},[33,2663,137],{"emptyLinePlaceholder":136},[33,2665,2666],{"class":35,"line":91},[33,2667,2668],{"class":39},"# ── bats doesn't have built-in parameterized tests ──\n",[33,2670,2671],{"class":35,"line":97},[33,2672,2673],{"class":39},"# But you can loop inside a test or generate tests with a helper.\n",[33,2675,2676],{"class":35,"line":109},[33,2677,137],{"emptyLinePlaceholder":136},[33,2679,2680],{"class":35,"line":133},[33,2681,2682],{"class":39},"# ── Method 1: loop inside a test ──\n",[33,2684,2685,2687,2690],{"class":35,"line":140},[33,2686,370],{"class":46},[33,2688,2689],{"class":50}," \"is_even for multiple values\"",[33,2691,376],{"class":50},[33,2693,2694,2696,2699,2702,2704,2707,2710,2713,2716,2719],{"class":35,"line":146},[33,2695,1256],{"class":227},[33,2697,2698],{"class":117}," n ",[33,2700,2701],{"class":227},"in",[33,2703,1209],{"class":50},[33,2705,2706],{"class":50}," 4",[33,2708,2709],{"class":50}," 6",[33,2711,2712],{"class":50}," 8",[33,2714,2715],{"class":50}," 10",[33,2717,2718],{"class":117},"; ",[33,2720,1281],{"class":227},[33,2722,2723,2726,2728,2730,2733],{"class":35,"line":159},[33,2724,2725],{"class":46},"        run",[33,2727,1525],{"class":50},[33,2729,124],{"class":50},[33,2731,2732],{"class":117},"$n",[33,2734,348],{"class":50},[33,2736,2737,2740,2742,2744,2746,2748,2750,2753,2756,2759,2762,2764],{"class":35,"line":172},[33,2738,2739],{"class":117},"        [[ ",[33,2741,318],{"class":50},[33,2743,579],{"class":117},[33,2745,318],{"class":50},[33,2747,407],{"class":227},[33,2749,657],{"class":68},[33,2751,2752],{"class":117}," ]] ",[33,2754,2755],{"class":227},"||",[33,2757,2758],{"class":46}," fail",[33,2760,2761],{"class":50}," \"is_even ",[33,2763,2732],{"class":117},[33,2765,2766],{"class":50}," should be even\"\n",[33,2768,2769],{"class":35,"line":177},[33,2770,1297],{"class":227},[33,2772,2773,2775,2777,2779,2781,2783,2785,2787,2790,2792],{"class":35,"line":183},[33,2774,1256],{"class":227},[33,2776,2698],{"class":117},[33,2778,2701],{"class":227},[33,2780,2134],{"class":50},[33,2782,800],{"class":50},[33,2784,410],{"class":50},[33,2786,2370],{"class":50},[33,2788,2789],{"class":50}," 9",[33,2791,2718],{"class":117},[33,2793,1281],{"class":227},[33,2795,2796,2798,2800,2802,2804],{"class":35,"line":189},[33,2797,2725],{"class":46},[33,2799,1525],{"class":50},[33,2801,124],{"class":50},[33,2803,2732],{"class":117},[33,2805,348],{"class":50},[33,2807,2808,2810,2812,2814,2816,2818,2820,2822,2824,2826,2828,2830],{"class":35,"line":195},[33,2809,2739],{"class":117},[33,2811,318],{"class":50},[33,2813,579],{"class":117},[33,2815,318],{"class":50},[33,2817,1583],{"class":227},[33,2819,657],{"class":68},[33,2821,2752],{"class":117},[33,2823,2755],{"class":227},[33,2825,2758],{"class":46},[33,2827,2761],{"class":50},[33,2829,2732],{"class":117},[33,2831,2832],{"class":50}," should be odd\"\n",[33,2834,2835],{"class":35,"line":200},[33,2836,1297],{"class":227},[33,2838,2839],{"class":35,"line":206},[33,2840,283],{"class":117},[33,2842,2843],{"class":35,"line":215},[33,2844,137],{"emptyLinePlaceholder":136},[33,2846,2847],{"class":35,"line":221},[33,2848,2849],{"class":39},"# ── Method 2: table-driven with data array ──\n",[33,2851,2852,2854,2857],{"class":35,"line":243},[33,2853,370],{"class":46},[33,2855,2856],{"class":50}," \"add table-driven\"",[33,2858,376],{"class":50},[33,2860,2861],{"class":35,"line":263},[33,2862,2863],{"class":39},"    # Format: \"a b expected\"\n",[33,2865,2866,2868,2871,2873],{"class":35,"line":280},[33,2867,1236],{"class":227},[33,2869,2870],{"class":117}," data",[33,2872,228],{"class":227},[33,2874,2875],{"class":117},"(\n",[33,2877,2878],{"class":35,"line":286},[33,2879,2880],{"class":50},"        \"1 2 3\"\n",[33,2882,2883],{"class":35,"line":291},[33,2884,2885],{"class":50},"        \"0 0 0\"\n",[33,2887,2888],{"class":35,"line":299},[33,2889,2890],{"class":50},"        \"-1 -1 -2\"\n",[33,2892,2893],{"class":35,"line":305},[33,2894,2895],{"class":50},"        \"100 200 300\"\n",[33,2897,2898],{"class":35,"line":351},[33,2899,2900],{"class":117},"    )\n",[33,2902,2903,2905,2908,2910,2913,2916,2918,2920,2922,2924],{"class":35,"line":356},[33,2904,1256],{"class":227},[33,2906,2907],{"class":117}," row ",[33,2909,2701],{"class":227},[33,2911,2912],{"class":50}," \"${",[33,2914,2915],{"class":117},"data",[33,2917,789],{"class":50},[33,2919,792],{"class":227},[33,2921,795],{"class":50},[33,2923,2718],{"class":117},[33,2925,1281],{"class":227},[33,2927,2928,2931,2934,2937,2940,2943,2946,2948,2951],{"class":35,"line":361},[33,2929,2930],{"class":68},"        read",[33,2932,2933],{"class":68}," -r",[33,2935,2936],{"class":50}," a",[33,2938,2939],{"class":50}," b",[33,2941,2942],{"class":50}," expected",[33,2944,2945],{"class":227}," \u003C\u003C\u003C",[33,2947,124],{"class":50},[33,2949,2950],{"class":117},"$row",[33,2952,348],{"class":50},[33,2954,2955,2958],{"class":35,"line":367},[33,2956,2957],{"class":227},"        local",[33,2959,2960],{"class":117}," actual\n",[33,2962,2963,2966,2968,2970,2972,2974,2977,2979,2981,2984,2986],{"class":35,"line":379},[33,2964,2965],{"class":117},"        actual",[33,2967,228],{"class":227},[33,2969,231],{"class":117},[33,2971,1159],{"class":46},[33,2973,124],{"class":50},[33,2975,2976],{"class":117},"$a",[33,2978,318],{"class":50},[33,2980,124],{"class":50},[33,2982,2983],{"class":117},"$b",[33,2985,318],{"class":50},[33,2987,240],{"class":117},[33,2989,2990,2992,2994,2997,2999,3001,3003,3006,3008,3010,3012,3014,3017,3019,3022,3025,3027,3030,3032],{"class":35,"line":390},[33,2991,2739],{"class":117},[33,2993,318],{"class":50},[33,2995,2996],{"class":117},"$actual",[33,2998,318],{"class":50},[33,3000,478],{"class":227},[33,3002,124],{"class":50},[33,3004,3005],{"class":117},"$expected",[33,3007,318],{"class":50},[33,3009,2752],{"class":117},[33,3011,2755],{"class":227},[33,3013,2758],{"class":46},[33,3015,3016],{"class":50}," \"add ",[33,3018,2976],{"class":117},[33,3020,3021],{"class":117}," $b",[33,3023,3024],{"class":50}," = ",[33,3026,2996],{"class":117},[33,3028,3029],{"class":50}," (expected ",[33,3031,3005],{"class":117},[33,3033,3034],{"class":50},")\"\n",[33,3036,3037],{"class":35,"line":416},[33,3038,1297],{"class":227},[33,3040,3041],{"class":35,"line":421},[33,3042,283],{"class":117},[33,3044,3045],{"class":35,"line":426},[33,3046,137],{"emptyLinePlaceholder":136},[33,3048,3049],{"class":35,"line":436},[33,3050,3051],{"class":39},"# ── Method 3: generate tests with bats-loop or a generator ──\n",[33,3053,3054],{"class":35,"line":466},[33,3055,3056],{"class":39},"# bats-loop: https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-loop\n",[33,3058,3059],{"class":35,"line":486},[33,3060,3061],{"class":39},"# Or generate .bats files from a template (for separate test per case)\n",[15,3063,3065],{"id":3064},"setup_file-and-teardown_file-bats-core-15","setup_file and teardown_file (bats-core 1.5+)",[20,3067,3068],{"language":22},[24,3069,3071],{"className":26,"code":3070,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bats\n\n# ── setup_file\u002Fteardown_file: run ONCE per file (not per test) ──\nsetup_file() {\n    # Heavy setup: start a test database, create fixtures\n    export TEST_DB=$(mktemp \u002Ftmp\u002Ftest.db.XXXXXX)\n    sqlite3 \"$TEST_DB\" \"CREATE TABLE users (id INTEGER, name TEXT);\"\n    sqlite3 \"$TEST_DB\" \"INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');\"\n}\n\nteardown_file() {\n    # Clean up after ALL tests\n    rm -f \"$TEST_DB\"\n}\n\n# ── setup\u002Fteardown: run before\u002Fafter EACH test ──\nsetup() {\n    # Per-test setup: reset state, create temp files\n    tmpdir=$(mktemp -d)\n}\n\nteardown() {\n    rm -rf -- \"$tmpdir\"\n}\n\n@test \"database has 2 users\" {\n    local count\n    count=$(sqlite3 \"$TEST_DB\" \"SELECT COUNT(*) FROM users;\")\n    [[ \"$count\" == \"2\" ]]\n}\n\n@test \"Alice exists in database\" {\n    local name\n    name=$(sqlite3 \"$TEST_DB\" \"SELECT name FROM users WHERE id=1;\")\n    [[ \"$name\" == \"Alice\" ]]\n}\n",[30,3072,3073,3077,3081,3086,3093,3098,3116,3131,3144,3148,3152,3159,3164,3177,3181,3185,3190,3196,3201,3215,3219,3223,3229,3243,3247,3251,3260,3267,3290,3308,3312,3316,3325,3332,3354,3372],{"__ignoreMap":28},[33,3074,3075],{"class":35,"line":36},[33,3076,192],{"class":39},[33,3078,3079],{"class":35,"line":43},[33,3080,137],{"emptyLinePlaceholder":136},[33,3082,3083],{"class":35,"line":60},[33,3084,3085],{"class":39},"# ── setup_file\u002Fteardown_file: run ONCE per file (not per test) ──\n",[33,3087,3088,3091],{"class":35,"line":78},[33,3089,3090],{"class":46},"setup_file",[33,3092,212],{"class":117},[33,3094,3095],{"class":35,"line":91},[33,3096,3097],{"class":39},"    # Heavy setup: start a test database, create fixtures\n",[33,3099,3100,3102,3105,3107,3109,3111,3114],{"class":35,"line":97},[33,3101,2232],{"class":227},[33,3103,3104],{"class":117}," TEST_DB",[33,3106,228],{"class":227},[33,3108,231],{"class":117},[33,3110,234],{"class":46},[33,3112,3113],{"class":50}," \u002Ftmp\u002Ftest.db.XXXXXX",[33,3115,240],{"class":117},[33,3117,3118,3121,3123,3126,3128],{"class":35,"line":109},[33,3119,3120],{"class":46},"    sqlite3",[33,3122,124],{"class":50},[33,3124,3125],{"class":117},"$TEST_DB",[33,3127,318],{"class":50},[33,3129,3130],{"class":50}," \"CREATE TABLE users (id INTEGER, name TEXT);\"\n",[33,3132,3133,3135,3137,3139,3141],{"class":35,"line":133},[33,3134,3120],{"class":46},[33,3136,124],{"class":50},[33,3138,3125],{"class":117},[33,3140,318],{"class":50},[33,3142,3143],{"class":50}," \"INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');\"\n",[33,3145,3146],{"class":35,"line":140},[33,3147,283],{"class":117},[33,3149,3150],{"class":35,"line":146},[33,3151,137],{"emptyLinePlaceholder":136},[33,3153,3154,3157],{"class":35,"line":159},[33,3155,3156],{"class":46},"teardown_file",[33,3158,212],{"class":117},[33,3160,3161],{"class":35,"line":172},[33,3162,3163],{"class":39},"    # Clean up after ALL tests\n",[33,3165,3166,3169,3171,3173,3175],{"class":35,"line":177},[33,3167,3168],{"class":46},"    rm",[33,3170,2235],{"class":68},[33,3172,124],{"class":50},[33,3174,3125],{"class":117},[33,3176,348],{"class":50},[33,3178,3179],{"class":35,"line":183},[33,3180,283],{"class":117},[33,3182,3183],{"class":35,"line":189},[33,3184,137],{"emptyLinePlaceholder":136},[33,3186,3187],{"class":35,"line":195},[33,3188,3189],{"class":39},"# ── setup\u002Fteardown: run before\u002Fafter EACH test ──\n",[33,3191,3192,3194],{"class":35,"line":200},[33,3193,209],{"class":46},[33,3195,212],{"class":117},[33,3197,3198],{"class":35,"line":206},[33,3199,3200],{"class":39},"    # Per-test setup: reset state, create temp files\n",[33,3202,3203,3205,3207,3209,3211,3213],{"class":35,"line":215},[33,3204,224],{"class":117},[33,3206,228],{"class":227},[33,3208,231],{"class":117},[33,3210,234],{"class":46},[33,3212,237],{"class":68},[33,3214,240],{"class":117},[33,3216,3217],{"class":35,"line":221},[33,3218,283],{"class":117},[33,3220,3221],{"class":35,"line":243},[33,3222,137],{"emptyLinePlaceholder":136},[33,3224,3225,3227],{"class":35,"line":263},[33,3226,294],{"class":46},[33,3228,212],{"class":117},[33,3230,3231,3233,3235,3237,3239,3241],{"class":35,"line":280},[33,3232,3168],{"class":46},[33,3234,338],{"class":68},[33,3236,341],{"class":68},[33,3238,124],{"class":50},[33,3240,257],{"class":117},[33,3242,348],{"class":50},[33,3244,3245],{"class":35,"line":286},[33,3246,283],{"class":117},[33,3248,3249],{"class":35,"line":291},[33,3250,137],{"emptyLinePlaceholder":136},[33,3252,3253,3255,3258],{"class":35,"line":299},[33,3254,370],{"class":46},[33,3256,3257],{"class":50}," \"database has 2 users\"",[33,3259,376],{"class":50},[33,3261,3262,3264],{"class":35,"line":305},[33,3263,1236],{"class":227},[33,3265,3266],{"class":117}," count\n",[33,3268,3269,3272,3274,3276,3279,3281,3283,3285,3288],{"class":35,"line":351},[33,3270,3271],{"class":117},"    count",[33,3273,228],{"class":227},[33,3275,231],{"class":117},[33,3277,3278],{"class":46},"sqlite3",[33,3280,124],{"class":50},[33,3282,3125],{"class":117},[33,3284,318],{"class":50},[33,3286,3287],{"class":50}," \"SELECT COUNT(*) FROM users;\"",[33,3289,240],{"class":117},[33,3291,3292,3294,3296,3299,3301,3303,3306],{"class":35,"line":356},[33,3293,308],{"class":117},[33,3295,318],{"class":50},[33,3297,3298],{"class":117},"$count",[33,3300,318],{"class":50},[33,3302,478],{"class":227},[33,3304,3305],{"class":50}," \"2\"",[33,3307,413],{"class":117},[33,3309,3310],{"class":35,"line":361},[33,3311,283],{"class":117},[33,3313,3314],{"class":35,"line":367},[33,3315,137],{"emptyLinePlaceholder":136},[33,3317,3318,3320,3323],{"class":35,"line":379},[33,3319,370],{"class":46},[33,3321,3322],{"class":50}," \"Alice exists in database\"",[33,3324,376],{"class":50},[33,3326,3327,3329],{"class":35,"line":390},[33,3328,1236],{"class":227},[33,3330,3331],{"class":117}," name\n",[33,3333,3334,3337,3339,3341,3343,3345,3347,3349,3352],{"class":35,"line":416},[33,3335,3336],{"class":117},"    name",[33,3338,228],{"class":227},[33,3340,231],{"class":117},[33,3342,3278],{"class":46},[33,3344,124],{"class":50},[33,3346,3125],{"class":117},[33,3348,318],{"class":50},[33,3350,3351],{"class":50}," \"SELECT name FROM users WHERE id=1;\"",[33,3353,240],{"class":117},[33,3355,3356,3358,3360,3363,3365,3367,3370],{"class":35,"line":421},[33,3357,308],{"class":117},[33,3359,318],{"class":50},[33,3361,3362],{"class":117},"$name",[33,3364,318],{"class":50},[33,3366,478],{"class":227},[33,3368,3369],{"class":50}," \"Alice\"",[33,3371,413],{"class":117},[33,3373,3374],{"class":35,"line":426},[33,3375,283],{"class":117},[15,3377,3379],{"id":3378},"anti-pattern-calling-the-function-twice","Anti-Pattern: Calling the Function Twice",[20,3381,3382],{"language":22},[24,3383,3385],{"className":26,"code":3384,"language":22,"meta":28,"style":28},"# ❌ NAIVE — calls add() twice (once with run, once with $())\n@test \"add returns the sum\" {\n    run add 2 3          # runs add, captures into $status\u002F$output (but doesn't check!)\n    [[ \"$(add 2 3)\" == \"5\" ]]  # runs add AGAIN — wasteful, and side effects compound!\n}\n\n# ✅ CORRECT — use run and check $output\n@test \"add returns the sum\" {\n    run add 2 3\n    assert_success\n    assert_output \"5\"    # checks $output (from the single run call)\n}\n\n# ✅ ALSO CORRECT — capture directly (for pure functions, no run needed)\n@test \"add returns the sum\" {\n    local result\n    result=$(add 2 3)\n    [[ \"$result\" == \"5\" ]]\n}\n\n# ── Why double-calling is bad ──\n# 1. Wasteful: runs the function twice.\n# 2. Side effects: if the function writes to a file or modifies state,\n#    calling it twice causes issues (double writes, double increments).\n# 3. Confusing: the run's output isn't checked; the direct call's result is.\n# Pick one: `run` + $status\u002F$output (for commands), or `$(...)` + check (for pure functions).\n",[30,3386,3387,3392,3400,3414,3438,3442,3446,3451,3459,3469,3473,3482,3486,3490,3495,3503,3510,3526,3542,3546,3550,3555,3560,3565,3570,3575],{"__ignoreMap":28},[33,3388,3389],{"class":35,"line":36},[33,3390,3391],{"class":39},"# ❌ NAIVE — calls add() twice (once with run, once with $())\n",[33,3393,3394,3396,3398],{"class":35,"line":43},[33,3395,370],{"class":46},[33,3397,1399],{"class":50},[33,3399,376],{"class":50},[33,3401,3402,3404,3407,3409,3411],{"class":35,"line":60},[33,3403,636],{"class":46},[33,3405,3406],{"class":50}," add",[33,3408,1209],{"class":68},[33,3410,800],{"class":68},[33,3412,3413],{"class":39},"          # runs add, captures into $status\u002F$output (but doesn't check!)\n",[33,3415,3416,3418,3420,3422,3424,3426,3428,3430,3432,3435],{"class":35,"line":78},[33,3417,308],{"class":117},[33,3419,548],{"class":50},[33,3421,1159],{"class":46},[33,3423,1209],{"class":68},[33,3425,800],{"class":68},[33,3427,1416],{"class":50},[33,3429,478],{"class":227},[33,3431,1421],{"class":50},[33,3433,3434],{"class":117}," ]]  ",[33,3436,3437],{"class":39},"# runs add AGAIN — wasteful, and side effects compound!\n",[33,3439,3440],{"class":35,"line":91},[33,3441,283],{"class":117},[33,3443,3444],{"class":35,"line":97},[33,3445,137],{"emptyLinePlaceholder":136},[33,3447,3448],{"class":35,"line":109},[33,3449,3450],{"class":39},"# ✅ CORRECT — use run and check $output\n",[33,3452,3453,3455,3457],{"class":35,"line":133},[33,3454,370],{"class":46},[33,3456,1399],{"class":50},[33,3458,376],{"class":50},[33,3460,3461,3463,3465,3467],{"class":35,"line":140},[33,3462,636],{"class":46},[33,3464,3406],{"class":50},[33,3466,1209],{"class":68},[33,3468,1570],{"class":68},[33,3470,3471],{"class":35,"line":146},[33,3472,2316],{"class":46},[33,3474,3475,3477,3479],{"class":35,"line":159},[33,3476,1929],{"class":46},[33,3478,1421],{"class":50},[33,3480,3481],{"class":39},"    # checks $output (from the single run call)\n",[33,3483,3484],{"class":35,"line":172},[33,3485,283],{"class":117},[33,3487,3488],{"class":35,"line":177},[33,3489,137],{"emptyLinePlaceholder":136},[33,3491,3492],{"class":35,"line":183},[33,3493,3494],{"class":39},"# ✅ ALSO CORRECT — capture directly (for pure functions, no run needed)\n",[33,3496,3497,3499,3501],{"class":35,"line":189},[33,3498,370],{"class":46},[33,3500,1399],{"class":50},[33,3502,376],{"class":50},[33,3504,3505,3507],{"class":35,"line":195},[33,3506,1236],{"class":227},[33,3508,3509],{"class":117}," result\n",[33,3511,3512,3514,3516,3518,3520,3522,3524],{"class":35,"line":200},[33,3513,439],{"class":117},[33,3515,228],{"class":227},[33,3517,231],{"class":117},[33,3519,1159],{"class":46},[33,3521,1209],{"class":68},[33,3523,800],{"class":68},[33,3525,240],{"class":117},[33,3527,3528,3530,3532,3534,3536,3538,3540],{"class":35,"line":206},[33,3529,308],{"class":117},[33,3531,318],{"class":50},[33,3533,473],{"class":117},[33,3535,318],{"class":50},[33,3537,478],{"class":227},[33,3539,1421],{"class":50},[33,3541,413],{"class":117},[33,3543,3544],{"class":35,"line":215},[33,3545,283],{"class":117},[33,3547,3548],{"class":35,"line":221},[33,3549,137],{"emptyLinePlaceholder":136},[33,3551,3552],{"class":35,"line":243},[33,3553,3554],{"class":39},"# ── Why double-calling is bad ──\n",[33,3556,3557],{"class":35,"line":263},[33,3558,3559],{"class":39},"# 1. Wasteful: runs the function twice.\n",[33,3561,3562],{"class":35,"line":280},[33,3563,3564],{"class":39},"# 2. Side effects: if the function writes to a file or modifies state,\n",[33,3566,3567],{"class":35,"line":286},[33,3568,3569],{"class":39},"#    calling it twice causes issues (double writes, double increments).\n",[33,3571,3572],{"class":35,"line":291},[33,3573,3574],{"class":39},"# 3. Confusing: the run's output isn't checked; the direct call's result is.\n",[33,3576,3577],{"class":35,"line":299},[33,3578,3579],{"class":39},"# Pick one: `run` + $status\u002F$output (for commands), or `$(...)` + check (for pure functions).\n",[15,3581,3583],{"id":3582},"ci-integration","CI Integration",[20,3585,3587],{"language":3586},"yaml",[24,3588,3591],{"className":3589,"code":3590,"language":3586,"meta":28,"style":28},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Ftest.yml\nname: Bash Tests\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - name: Install bats-core\n        run: |\n          git clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-core.git\n          cd bats-core && .\u002Finstall.sh \u002Fusr\u002Flocal\n\n      - name: Install bats-assert and bats-support\n        run: |\n          git clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-support.git test\u002Ftest_helper\u002Fbats-support\n          git clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-assert.git test\u002Ftest_helper\u002Fbats-assert\n\n      - name: Install ShellCheck\n        run: sudo apt-get install -y shellcheck\n\n      - name: Run ShellCheck\n        run: shellcheck *.sh lib\u002F*.sh\n        # Lint all shell scripts\n\n      - name: Run bats tests\n        run: bats test\u002F\n        # Run all .bats files in test\u002F\n\n      - name: Run bats tests (TAP output)\n        run: bats --formatter tap test\u002F\n        # TAP format for CI integration\n",[30,3592,3593,3598,3610,3629,3633,3641,3648,3658,3665,3678,3682,3693,3702,3707,3712,3716,3727,3735,3740,3745,3749,3760,3769,3773,3784,3793,3798,3802,3813,3822,3827,3831,3842,3851],{"__ignoreMap":28},[33,3594,3595],{"class":35,"line":36},[33,3596,3597],{"class":39},"# .github\u002Fworkflows\u002Ftest.yml\n",[33,3599,3600,3604,3607],{"class":35,"line":43},[33,3601,3603],{"class":3602},"sk71V","name",[33,3605,3606],{"class":117},": ",[33,3608,3609],{"class":50},"Bash Tests\n",[33,3611,3612,3615,3618,3621,3623,3626],{"class":35,"line":60},[33,3613,3614],{"class":68},"on",[33,3616,3617],{"class":117},": [",[33,3619,3620],{"class":50},"push",[33,3622,576],{"class":117},[33,3624,3625],{"class":50},"pull_request",[33,3627,3628],{"class":117},"]\n",[33,3630,3631],{"class":35,"line":78},[33,3632,137],{"emptyLinePlaceholder":136},[33,3634,3635,3638],{"class":35,"line":91},[33,3636,3637],{"class":3602},"jobs",[33,3639,3640],{"class":117},":\n",[33,3642,3643,3646],{"class":35,"line":97},[33,3644,3645],{"class":3602},"  test",[33,3647,3640],{"class":117},[33,3649,3650,3653,3655],{"class":35,"line":109},[33,3651,3652],{"class":3602},"    runs-on",[33,3654,3606],{"class":117},[33,3656,3657],{"class":50},"ubuntu-latest\n",[33,3659,3660,3663],{"class":35,"line":133},[33,3661,3662],{"class":3602},"    steps",[33,3664,3640],{"class":117},[33,3666,3667,3670,3673,3675],{"class":35,"line":140},[33,3668,3669],{"class":117},"      - ",[33,3671,3672],{"class":3602},"uses",[33,3674,3606],{"class":117},[33,3676,3677],{"class":50},"actions\u002Fcheckout@v4\n",[33,3679,3680],{"class":35,"line":146},[33,3681,137],{"emptyLinePlaceholder":136},[33,3683,3684,3686,3688,3690],{"class":35,"line":159},[33,3685,3669],{"class":117},[33,3687,3603],{"class":3602},[33,3689,3606],{"class":117},[33,3691,3692],{"class":50},"Install bats-core\n",[33,3694,3695,3697,3699],{"class":35,"line":172},[33,3696,2725],{"class":3602},[33,3698,3606],{"class":117},[33,3700,3701],{"class":227},"|\n",[33,3703,3704],{"class":35,"line":177},[33,3705,3706],{"class":50},"          git clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-core.git\n",[33,3708,3709],{"class":35,"line":183},[33,3710,3711],{"class":50},"          cd bats-core && .\u002Finstall.sh \u002Fusr\u002Flocal\n",[33,3713,3714],{"class":35,"line":189},[33,3715,137],{"emptyLinePlaceholder":136},[33,3717,3718,3720,3722,3724],{"class":35,"line":195},[33,3719,3669],{"class":117},[33,3721,3603],{"class":3602},[33,3723,3606],{"class":117},[33,3725,3726],{"class":50},"Install bats-assert and bats-support\n",[33,3728,3729,3731,3733],{"class":35,"line":200},[33,3730,2725],{"class":3602},[33,3732,3606],{"class":117},[33,3734,3701],{"class":227},[33,3736,3737],{"class":35,"line":206},[33,3738,3739],{"class":50},"          git clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-support.git test\u002Ftest_helper\u002Fbats-support\n",[33,3741,3742],{"class":35,"line":215},[33,3743,3744],{"class":50},"          git clone https:\u002F\u002Fgithub.com\u002Fbats-core\u002Fbats-assert.git test\u002Ftest_helper\u002Fbats-assert\n",[33,3746,3747],{"class":35,"line":221},[33,3748,137],{"emptyLinePlaceholder":136},[33,3750,3751,3753,3755,3757],{"class":35,"line":243},[33,3752,3669],{"class":117},[33,3754,3603],{"class":3602},[33,3756,3606],{"class":117},[33,3758,3759],{"class":50},"Install ShellCheck\n",[33,3761,3762,3764,3766],{"class":35,"line":263},[33,3763,2725],{"class":3602},[33,3765,3606],{"class":117},[33,3767,3768],{"class":50},"sudo apt-get install -y shellcheck\n",[33,3770,3771],{"class":35,"line":280},[33,3772,137],{"emptyLinePlaceholder":136},[33,3774,3775,3777,3779,3781],{"class":35,"line":286},[33,3776,3669],{"class":117},[33,3778,3603],{"class":3602},[33,3780,3606],{"class":117},[33,3782,3783],{"class":50},"Run ShellCheck\n",[33,3785,3786,3788,3790],{"class":35,"line":291},[33,3787,2725],{"class":3602},[33,3789,3606],{"class":117},[33,3791,3792],{"class":50},"shellcheck *.sh lib\u002F*.sh\n",[33,3794,3795],{"class":35,"line":299},[33,3796,3797],{"class":39},"        # Lint all shell scripts\n",[33,3799,3800],{"class":35,"line":305},[33,3801,137],{"emptyLinePlaceholder":136},[33,3803,3804,3806,3808,3810],{"class":35,"line":351},[33,3805,3669],{"class":117},[33,3807,3603],{"class":3602},[33,3809,3606],{"class":117},[33,3811,3812],{"class":50},"Run bats tests\n",[33,3814,3815,3817,3819],{"class":35,"line":356},[33,3816,2725],{"class":3602},[33,3818,3606],{"class":117},[33,3820,3821],{"class":50},"bats test\u002F\n",[33,3823,3824],{"class":35,"line":361},[33,3825,3826],{"class":39},"        # Run all .bats files in test\u002F\n",[33,3828,3829],{"class":35,"line":367},[33,3830,137],{"emptyLinePlaceholder":136},[33,3832,3833,3835,3837,3839],{"class":35,"line":379},[33,3834,3669],{"class":117},[33,3836,3603],{"class":3602},[33,3838,3606],{"class":117},[33,3840,3841],{"class":50},"Run bats tests (TAP output)\n",[33,3843,3844,3846,3848],{"class":35,"line":390},[33,3845,2725],{"class":3602},[33,3847,3606],{"class":117},[33,3849,3850],{"class":50},"bats --formatter tap test\u002F\n",[33,3852,3853],{"class":35,"line":416},[33,3854,3855],{"class":39},"        # TAP format for CI integration\n",[15,3857,3859],{"id":3858},"production-pattern-testing-a-complete-script","Production Pattern: Testing a Complete Script",[20,3861,3862],{"language":22},[24,3863,3865],{"className":26,"code":3864,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bats\n\n# ── Test the full backup script ──\n# bin\u002Fbackup.sh:\n#   #!\u002Fusr\u002Fbin\u002Fenv bash\n#   set -euo pipefail\n#   src=\"${1:-}\"\n#   dst=\"${2:-}\"\n#   [[ -d \"$src\" ]] || { echo \"source not found: $src\" >&2; exit 1; }\n#   [[ -n \"$dst\" ]] || { echo \"missing dest\" >&2; exit 1; }\n#   tar -czf \"$dst\u002Fbackup.tar.gz\" -C \"$src\" .\n#   echo \"backup complete\"\n\nsetup() {\n    src=$(mktemp -d)\n    dst=$(mktemp -d)\n    echo \"important data\" > \"$src\u002Ffile.txt\"\n}\n\nteardown() {\n    rm -rf -- \"$src\" \"$dst\"\n}\n\n@test \"creates a backup\" {\n    run bash \"$BATS_TEST_DIRNAME\u002F..\u002Fbin\u002Fbackup.sh\" \"$src\" \"$dst\"\n    assert_success\n    assert_output --partial \"backup complete\"\n    [[ -f \"$dst\u002Fbackup.tar.gz\" ]]\n    # Verify the backup content\n    tar -xzf \"$dst\u002Fbackup.tar.gz\" -C \"$dst\u002Fextracted\"\n    [[ \"$(cat \"$dst\u002Fextracted\u002Ffile.txt\")\" == \"important data\" ]]\n}\n\n@test \"fails on missing source\" {\n    run bash \"$BATS_TEST_DIRNAME\u002F..\u002Fbin\u002Fbackup.sh\" \"\u002Fnonexistent\" \"$dst\"\n    assert_failure\n    assert_output --partial \"source not found\"\n}\n\n@test \"fails on missing dest\" {\n    run bash \"$BATS_TEST_DIRNAME\u002F..\u002Fbin\u002Fbackup.sh\" \"$src\" \"\"\n    assert_failure\n    assert_output --partial \"missing dest\"\n}\n\n@test \"fails on non-directory source\" {\n    echo \"not a dir\" > \"$src\u002Ffile2.txt\"\n    run bash \"$BATS_TEST_DIRNAME\u002F..\u002Fbin\u002Fbackup.sh\" \"$src\u002Ffile2.txt\" \"$dst\"\n    assert_failure\n    assert_output --partial \"source not found\"\n}\n",[30,3866,3867,3871,3875,3880,3885,3890,3895,3900,3905,3910,3915,3920,3925,3929,3935,3950,3965,3982,3986,3990,3996,4017,4021,4025,4034,4060,4064,4073,4088,4093,4117,4138,4142,4146,4155,4176,4180,4189,4193,4197,4206,4227,4231,4240,4244,4248,4257,4273,4298,4302,4310],{"__ignoreMap":28},[33,3868,3869],{"class":35,"line":36},[33,3870,192],{"class":39},[33,3872,3873],{"class":35,"line":43},[33,3874,137],{"emptyLinePlaceholder":136},[33,3876,3877],{"class":35,"line":60},[33,3878,3879],{"class":39},"# ── Test the full backup script ──\n",[33,3881,3882],{"class":35,"line":78},[33,3883,3884],{"class":39},"# bin\u002Fbackup.sh:\n",[33,3886,3887],{"class":35,"line":91},[33,3888,3889],{"class":39},"#   #!\u002Fusr\u002Fbin\u002Fenv bash\n",[33,3891,3892],{"class":35,"line":97},[33,3893,3894],{"class":39},"#   set -euo pipefail\n",[33,3896,3897],{"class":35,"line":109},[33,3898,3899],{"class":39},"#   src=\"${1:-}\"\n",[33,3901,3902],{"class":35,"line":133},[33,3903,3904],{"class":39},"#   dst=\"${2:-}\"\n",[33,3906,3907],{"class":35,"line":140},[33,3908,3909],{"class":39},"#   [[ -d \"$src\" ]] || { echo \"source not found: $src\" >&2; exit 1; }\n",[33,3911,3912],{"class":35,"line":146},[33,3913,3914],{"class":39},"#   [[ -n \"$dst\" ]] || { echo \"missing dest\" >&2; exit 1; }\n",[33,3916,3917],{"class":35,"line":159},[33,3918,3919],{"class":39},"#   tar -czf \"$dst\u002Fbackup.tar.gz\" -C \"$src\" .\n",[33,3921,3922],{"class":35,"line":172},[33,3923,3924],{"class":39},"#   echo \"backup complete\"\n",[33,3926,3927],{"class":35,"line":177},[33,3928,137],{"emptyLinePlaceholder":136},[33,3930,3931,3933],{"class":35,"line":183},[33,3932,209],{"class":46},[33,3934,212],{"class":117},[33,3936,3937,3940,3942,3944,3946,3948],{"class":35,"line":189},[33,3938,3939],{"class":117},"    src",[33,3941,228],{"class":227},[33,3943,231],{"class":117},[33,3945,234],{"class":46},[33,3947,237],{"class":68},[33,3949,240],{"class":117},[33,3951,3952,3955,3957,3959,3961,3963],{"class":35,"line":195},[33,3953,3954],{"class":117},"    dst",[33,3956,228],{"class":227},[33,3958,231],{"class":117},[33,3960,234],{"class":46},[33,3962,237],{"class":68},[33,3964,240],{"class":117},[33,3966,3967,3969,3972,3974,3976,3979],{"class":35,"line":200},[33,3968,246],{"class":68},[33,3970,3971],{"class":50}," \"important data\"",[33,3973,252],{"class":227},[33,3975,124],{"class":50},[33,3977,3978],{"class":117},"$src",[33,3980,3981],{"class":50},"\u002Ffile.txt\"\n",[33,3983,3984],{"class":35,"line":206},[33,3985,283],{"class":117},[33,3987,3988],{"class":35,"line":215},[33,3989,137],{"emptyLinePlaceholder":136},[33,3991,3992,3994],{"class":35,"line":221},[33,3993,294],{"class":46},[33,3995,212],{"class":117},[33,3997,3998,4000,4002,4004,4006,4008,4010,4012,4015],{"class":35,"line":243},[33,3999,3168],{"class":46},[33,4001,338],{"class":68},[33,4003,341],{"class":68},[33,4005,124],{"class":50},[33,4007,3978],{"class":117},[33,4009,318],{"class":50},[33,4011,124],{"class":50},[33,4013,4014],{"class":117},"$dst",[33,4016,348],{"class":50},[33,4018,4019],{"class":35,"line":263},[33,4020,283],{"class":117},[33,4022,4023],{"class":35,"line":280},[33,4024,137],{"emptyLinePlaceholder":136},[33,4026,4027,4029,4032],{"class":35,"line":286},[33,4028,370],{"class":46},[33,4030,4031],{"class":50}," \"creates a backup\"",[33,4033,376],{"class":50},[33,4035,4036,4038,4040,4042,4045,4048,4050,4052,4054,4056,4058],{"class":35,"line":291},[33,4037,636],{"class":46},[33,4039,700],{"class":50},[33,4041,124],{"class":50},[33,4043,4044],{"class":117},"$BATS_TEST_DIRNAME",[33,4046,4047],{"class":50},"\u002F..\u002Fbin\u002Fbackup.sh\"",[33,4049,124],{"class":50},[33,4051,3978],{"class":117},[33,4053,318],{"class":50},[33,4055,124],{"class":50},[33,4057,4014],{"class":117},[33,4059,348],{"class":50},[33,4061,4062],{"class":35,"line":299},[33,4063,2316],{"class":46},[33,4065,4066,4068,4070],{"class":35,"line":305},[33,4067,1929],{"class":46},[33,4069,1964],{"class":68},[33,4071,4072],{"class":50}," \"backup complete\"\n",[33,4074,4075,4077,4079,4081,4083,4086],{"class":35,"line":351},[33,4076,308],{"class":117},[33,4078,511],{"class":227},[33,4080,124],{"class":50},[33,4082,4014],{"class":117},[33,4084,4085],{"class":50},"\u002Fbackup.tar.gz\"",[33,4087,413],{"class":117},[33,4089,4090],{"class":35,"line":356},[33,4091,4092],{"class":39},"    # Verify the backup content\n",[33,4094,4095,4098,4101,4103,4105,4107,4110,4112,4114],{"class":35,"line":361},[33,4096,4097],{"class":46},"    tar",[33,4099,4100],{"class":68}," -xzf",[33,4102,124],{"class":50},[33,4104,4014],{"class":117},[33,4106,4085],{"class":50},[33,4108,4109],{"class":68}," -C",[33,4111,124],{"class":50},[33,4113,4014],{"class":117},[33,4115,4116],{"class":50},"\u002Fextracted\"\n",[33,4118,4119,4121,4123,4125,4127,4129,4132,4134,4136],{"class":35,"line":367},[33,4120,308],{"class":117},[33,4122,548],{"class":50},[33,4124,551],{"class":46},[33,4126,124],{"class":50},[33,4128,4014],{"class":117},[33,4130,4131],{"class":50},"\u002Fextracted\u002Ffile.txt\")\"",[33,4133,478],{"class":227},[33,4135,3971],{"class":50},[33,4137,413],{"class":117},[33,4139,4140],{"class":35,"line":379},[33,4141,283],{"class":117},[33,4143,4144],{"class":35,"line":390},[33,4145,137],{"emptyLinePlaceholder":136},[33,4147,4148,4150,4153],{"class":35,"line":416},[33,4149,370],{"class":46},[33,4151,4152],{"class":50}," \"fails on missing source\"",[33,4154,376],{"class":50},[33,4156,4157,4159,4161,4163,4165,4167,4170,4172,4174],{"class":35,"line":421},[33,4158,636],{"class":46},[33,4160,700],{"class":50},[33,4162,124],{"class":50},[33,4164,4044],{"class":117},[33,4166,4047],{"class":50},[33,4168,4169],{"class":50}," \"\u002Fnonexistent\"",[33,4171,124],{"class":50},[33,4173,4014],{"class":117},[33,4175,348],{"class":50},[33,4177,4178],{"class":35,"line":426},[33,4179,2402],{"class":46},[33,4181,4182,4184,4186],{"class":35,"line":436},[33,4183,1929],{"class":46},[33,4185,1964],{"class":68},[33,4187,4188],{"class":50}," \"source not found\"\n",[33,4190,4191],{"class":35,"line":466},[33,4192,283],{"class":117},[33,4194,4195],{"class":35,"line":486},[33,4196,137],{"emptyLinePlaceholder":136},[33,4198,4199,4201,4204],{"class":35,"line":491},[33,4200,370],{"class":46},[33,4202,4203],{"class":50}," \"fails on missing dest\"",[33,4205,376],{"class":50},[33,4207,4208,4210,4212,4214,4216,4218,4220,4222,4224],{"class":35,"line":496},[33,4209,636],{"class":46},[33,4211,700],{"class":50},[33,4213,124],{"class":50},[33,4215,4044],{"class":117},[33,4217,4047],{"class":50},[33,4219,124],{"class":50},[33,4221,3978],{"class":117},[33,4223,318],{"class":50},[33,4225,4226],{"class":50}," \"\"\n",[33,4228,4229],{"class":35,"line":506},[33,4230,2402],{"class":46},[33,4232,4233,4235,4237],{"class":35,"line":523},[33,4234,1929],{"class":46},[33,4236,1964],{"class":68},[33,4238,4239],{"class":50}," \"missing dest\"\n",[33,4241,4242],{"class":35,"line":528},[33,4243,283],{"class":117},[33,4245,4246],{"class":35,"line":533},[33,4247,137],{"emptyLinePlaceholder":136},[33,4249,4250,4252,4255],{"class":35,"line":543},[33,4251,370],{"class":46},[33,4253,4254],{"class":50}," \"fails on non-directory source\"",[33,4256,376],{"class":50},[33,4258,4259,4261,4264,4266,4268,4270],{"class":35,"line":567},[33,4260,246],{"class":68},[33,4262,4263],{"class":50}," \"not a dir\"",[33,4265,252],{"class":227},[33,4267,124],{"class":50},[33,4269,3978],{"class":117},[33,4271,4272],{"class":50},"\u002Ffile2.txt\"\n",[33,4274,4275,4277,4279,4281,4283,4285,4287,4289,4292,4294,4296],{"class":35,"line":1084},[33,4276,636],{"class":46},[33,4278,700],{"class":50},[33,4280,124],{"class":50},[33,4282,4044],{"class":117},[33,4284,4047],{"class":50},[33,4286,124],{"class":50},[33,4288,3978],{"class":117},[33,4290,4291],{"class":50},"\u002Ffile2.txt\"",[33,4293,124],{"class":50},[33,4295,4014],{"class":117},[33,4297,348],{"class":50},[33,4299,4300],{"class":35,"line":1099},[33,4301,2402],{"class":46},[33,4303,4304,4306,4308],{"class":35,"line":1117},[33,4305,1929],{"class":46},[33,4307,1964],{"class":68},[33,4309,4188],{"class":50},[33,4311,4312],{"class":35,"line":1136},[33,4313,283],{"class":117},[15,4315,4317],{"id":4316},"tips-tricks","💡 Tips & Tricks",[20,4319,4320],{"language":22},[24,4321,4323],{"className":26,"code":4322,"language":22,"meta":28,"style":28},"# ── Skip tests conditionally ──\n@test \"only run on Linux\" {\n    [[ \"$(uname)\" == \"Linux\" ]] || skip \"Linux only\"\n    # ... test ...\n}\n\n@test \"requires docker\" {\n    command -v docker &>\u002Fdev\u002Fnull || skip \"docker not installed\"\n    # ... test ...\n}\n\n@test \"TODO: implement\" {\n    skip \"not implemented yet\"\n}\n\n# ── Skip with reason ──\n@test \"slow integration test\" {\n    [[ -n \"${RUN_SLOW:-}\" ]] || skip \"set RUN_SLOW=1 to run\"\n    # ... slow test ...\n}\n\n# ── Test helpers: create reusable functions ──\nsetup() {\n    # Create a temp project with a git repo\n    setup_git_repo() {\n        local dir=$1\n        mkdir -p \"$dir\"\n        cd \"$dir\"\n        git init --quiet\n        git config user.email \"test@test.com\"\n        git config user.name \"Test\"\n        echo \"content\" > file.txt\n        git add . && git commit -m \"initial\" --quiet\n    }\n    export -f setup_git_repo\n}\n\n# ── Capture output for debugging ──\n@test \"debug output\" {\n    run .\u002Fmy_script.sh\n    # If the test fails, bats shows $output and $status\n    # You can also print manually:\n    echo \"DEBUG: status=$status\" >&3   # >&3 prints to bats' output (not captured)\n    echo \"DEBUG: output=$output\" >&3\n    assert_success\n}\n\n# ── Run a subset of tests ──\n# bats test\u002Fmath_test.bats        # run one file\n# bats -f \"factorial\" test\u002F       # run tests matching filter\n# bats -n test\u002F                   # run only tests that failed last time (junit output)\n",[30,4324,4325,4330,4339,4365,4370,4374,4378,4387,4414,4418,4422,4426,4435,4443,4447,4451,4456,4465,4490,4495,4499,4503,4508,4514,4519,4526,4538,4553,4564,4575,4588,4600,4612,4636,4640,4649,4653,4657,4662,4671,4678,4683,4688,4705,4719,4723,4727,4731,4736,4741,4746],{"__ignoreMap":28},[33,4326,4327],{"class":35,"line":36},[33,4328,4329],{"class":39},"# ── Skip tests conditionally ──\n",[33,4331,4332,4334,4337],{"class":35,"line":43},[33,4333,370],{"class":46},[33,4335,4336],{"class":50}," \"only run on Linux\"",[33,4338,376],{"class":50},[33,4340,4341,4343,4345,4348,4350,4352,4355,4357,4359,4362],{"class":35,"line":60},[33,4342,308],{"class":117},[33,4344,548],{"class":50},[33,4346,4347],{"class":46},"uname",[33,4349,1416],{"class":50},[33,4351,478],{"class":227},[33,4353,4354],{"class":50}," \"Linux\"",[33,4356,2752],{"class":117},[33,4358,2755],{"class":227},[33,4360,4361],{"class":46}," skip",[33,4363,4364],{"class":50}," \"Linux only\"\n",[33,4366,4367],{"class":35,"line":78},[33,4368,4369],{"class":39},"    # ... test ...\n",[33,4371,4372],{"class":35,"line":91},[33,4373,283],{"class":117},[33,4375,4376],{"class":35,"line":97},[33,4377,137],{"emptyLinePlaceholder":136},[33,4379,4380,4382,4385],{"class":35,"line":109},[33,4381,370],{"class":46},[33,4383,4384],{"class":50}," \"requires docker\"",[33,4386,376],{"class":50},[33,4388,4389,4392,4395,4398,4401,4404,4407,4409,4411],{"class":35,"line":133},[33,4390,4391],{"class":68},"    command",[33,4393,4394],{"class":68}," -v",[33,4396,4397],{"class":50}," docker",[33,4399,4400],{"class":117}," &",[33,4402,4403],{"class":227},">",[33,4405,4406],{"class":117},"\u002Fdev\u002Fnull ",[33,4408,2755],{"class":227},[33,4410,4361],{"class":46},[33,4412,4413],{"class":50}," \"docker not installed\"\n",[33,4415,4416],{"class":35,"line":140},[33,4417,4369],{"class":39},[33,4419,4420],{"class":35,"line":146},[33,4421,283],{"class":117},[33,4423,4424],{"class":35,"line":159},[33,4425,137],{"emptyLinePlaceholder":136},[33,4427,4428,4430,4433],{"class":35,"line":172},[33,4429,370],{"class":46},[33,4431,4432],{"class":50}," \"TODO: implement\"",[33,4434,376],{"class":50},[33,4436,4437,4440],{"class":35,"line":177},[33,4438,4439],{"class":46},"    skip",[33,4441,4442],{"class":50}," \"not implemented yet\"\n",[33,4444,4445],{"class":35,"line":183},[33,4446,283],{"class":117},[33,4448,4449],{"class":35,"line":189},[33,4450,137],{"emptyLinePlaceholder":136},[33,4452,4453],{"class":35,"line":195},[33,4454,4455],{"class":39},"# ── Skip with reason ──\n",[33,4457,4458,4460,4463],{"class":35,"line":200},[33,4459,370],{"class":46},[33,4461,4462],{"class":50}," \"slow integration test\"",[33,4464,376],{"class":50},[33,4466,4467,4469,4471,4473,4476,4479,4481,4483,4485,4487],{"class":35,"line":206},[33,4468,308],{"class":117},[33,4470,311],{"class":227},[33,4472,2912],{"class":50},[33,4474,4475],{"class":117},"RUN_SLOW",[33,4477,4478],{"class":227},":-",[33,4480,404],{"class":50},[33,4482,2752],{"class":117},[33,4484,2755],{"class":227},[33,4486,4361],{"class":46},[33,4488,4489],{"class":50}," \"set RUN_SLOW=1 to run\"\n",[33,4491,4492],{"class":35,"line":215},[33,4493,4494],{"class":39},"    # ... slow test ...\n",[33,4496,4497],{"class":35,"line":221},[33,4498,283],{"class":117},[33,4500,4501],{"class":35,"line":243},[33,4502,137],{"emptyLinePlaceholder":136},[33,4504,4505],{"class":35,"line":263},[33,4506,4507],{"class":39},"# ── Test helpers: create reusable functions ──\n",[33,4509,4510,4512],{"class":35,"line":280},[33,4511,209],{"class":46},[33,4513,212],{"class":117},[33,4515,4516],{"class":35,"line":286},[33,4517,4518],{"class":39},"    # Create a temp project with a git repo\n",[33,4520,4521,4524],{"class":35,"line":291},[33,4522,4523],{"class":46},"    setup_git_repo",[33,4525,212],{"class":117},[33,4527,4528,4530,4533,4535],{"class":35,"line":299},[33,4529,2957],{"class":227},[33,4531,4532],{"class":117}," dir",[33,4534,228],{"class":227},[33,4536,4537],{"class":1203},"$1\n",[33,4539,4540,4543,4546,4548,4551],{"class":35,"line":305},[33,4541,4542],{"class":46},"        mkdir",[33,4544,4545],{"class":68}," -p",[33,4547,124],{"class":50},[33,4549,4550],{"class":117},"$dir",[33,4552,348],{"class":50},[33,4554,4555,4558,4560,4562],{"class":35,"line":351},[33,4556,4557],{"class":68},"        cd",[33,4559,124],{"class":50},[33,4561,4550],{"class":117},[33,4563,348],{"class":50},[33,4565,4566,4569,4572],{"class":35,"line":356},[33,4567,4568],{"class":46},"        git",[33,4570,4571],{"class":50}," init",[33,4573,4574],{"class":68}," --quiet\n",[33,4576,4577,4579,4582,4585],{"class":35,"line":361},[33,4578,4568],{"class":46},[33,4580,4581],{"class":50}," config",[33,4583,4584],{"class":50}," user.email",[33,4586,4587],{"class":50}," \"test@test.com\"\n",[33,4589,4590,4592,4594,4597],{"class":35,"line":367},[33,4591,4568],{"class":46},[33,4593,4581],{"class":50},[33,4595,4596],{"class":50}," user.name",[33,4598,4599],{"class":50}," \"Test\"\n",[33,4601,4602,4604,4607,4609],{"class":35,"line":379},[33,4603,2211],{"class":68},[33,4605,4606],{"class":50}," \"content\"",[33,4608,252],{"class":227},[33,4610,4611],{"class":50}," file.txt\n",[33,4613,4614,4616,4618,4621,4623,4625,4628,4631,4634],{"class":35,"line":390},[33,4615,4568],{"class":46},[33,4617,3406],{"class":50},[33,4619,4620],{"class":50}," .",[33,4622,118],{"class":117},[33,4624,100],{"class":46},[33,4626,4627],{"class":50}," commit",[33,4629,4630],{"class":68}," -m",[33,4632,4633],{"class":50}," \"initial\"",[33,4635,4574],{"class":68},[33,4637,4638],{"class":35,"line":416},[33,4639,2227],{"class":117},[33,4641,4642,4644,4646],{"class":35,"line":421},[33,4643,2232],{"class":227},[33,4645,2235],{"class":68},[33,4647,4648],{"class":117}," setup_git_repo\n",[33,4650,4651],{"class":35,"line":426},[33,4652,283],{"class":117},[33,4654,4655],{"class":35,"line":436},[33,4656,137],{"emptyLinePlaceholder":136},[33,4658,4659],{"class":35,"line":466},[33,4660,4661],{"class":39},"# ── Capture output for debugging ──\n",[33,4663,4664,4666,4669],{"class":35,"line":486},[33,4665,370],{"class":46},[33,4667,4668],{"class":50}," \"debug output\"",[33,4670,376],{"class":50},[33,4672,4673,4675],{"class":35,"line":491},[33,4674,636],{"class":46},[33,4676,4677],{"class":50}," .\u002Fmy_script.sh\n",[33,4679,4680],{"class":35,"line":496},[33,4681,4682],{"class":39},"    # If the test fails, bats shows $output and $status\n",[33,4684,4685],{"class":35,"line":506},[33,4686,4687],{"class":39},"    # You can also print manually:\n",[33,4689,4690,4692,4695,4697,4699,4702],{"class":35,"line":523},[33,4691,246],{"class":68},[33,4693,4694],{"class":50}," \"DEBUG: status=",[33,4696,579],{"class":117},[33,4698,318],{"class":50},[33,4700,4701],{"class":227}," >&3",[33,4703,4704],{"class":39},"   # >&3 prints to bats' output (not captured)\n",[33,4706,4707,4709,4712,4714,4716],{"class":35,"line":528},[33,4708,246],{"class":68},[33,4710,4711],{"class":50}," \"DEBUG: output=",[33,4713,582],{"class":117},[33,4715,318],{"class":50},[33,4717,4718],{"class":227}," >&3\n",[33,4720,4721],{"class":35,"line":533},[33,4722,2316],{"class":46},[33,4724,4725],{"class":35,"line":543},[33,4726,283],{"class":117},[33,4728,4729],{"class":35,"line":567},[33,4730,137],{"emptyLinePlaceholder":136},[33,4732,4733],{"class":35,"line":1084},[33,4734,4735],{"class":39},"# ── Run a subset of tests ──\n",[33,4737,4738],{"class":35,"line":1099},[33,4739,4740],{"class":39},"# bats test\u002Fmath_test.bats        # run one file\n",[33,4742,4743],{"class":35,"line":1117},[33,4744,4745],{"class":39},"# bats -f \"factorial\" test\u002F       # run tests matching filter\n",[33,4747,4748],{"class":35,"line":1136},[33,4749,4750],{"class":39},"# bats -n test\u002F                   # run only tests that failed last time (junit output)\n",[15,4752,4754],{"id":4753},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[20,4756,4757],{"language":22},[24,4758,4760],{"className":26,"code":4759,"language":22,"meta":28,"style":28},"# ── bats tests are in .bats files ──\n# bats test\u002F runs all *.bats files. .sh test files are ignored.\n# Name test files: test\u002Ffeature_test.bats\n\n# ── `source` in a test runs in the test's shell ──\n# Functions\u002Fvariables from the sourced file are available.\n# But `set -e` in the sourced file applies to the test shell (may cause unexpected exits).\n# Don't use `set -e` in libraries — use it only in scripts.\n\n# ── `run` captures stdout AND stderr ──\n# $output has both. Use `run --separate-stderr` (bats 1.5+) for $stderr separately.\n\n# ── Mocking with functions affects the test shell ──\n# `git() { ... }` overrides git in the test. Always unset in teardown: `unset -f git`.\n\n# ── `$BATS_TEST_FILENAME` vs `$0` ──\n# $0 is the bats executable. Use $BATS_TEST_FILENAME for the test file's path.\n# source \"$(dirname \"$BATS_TEST_FILENAME\")\u002F..\u002Flib\u002Futils.sh\"\n\n# ── bats and `set -e` ──\n# A failing `[[ ]]` in a @test causes the test to fail (good).\n# But `set -e` in a sourced file can exit the test shell. Don't source files with `set -e`.\n\n# ── Tests don't catch everything ──\n# bats tests behavior (runtime). ShellCheck catches static issues (quoting, unset vars).\n# Use BOTH: bats for behavior, ShellCheck for static analysis.\n",[30,4761,4762,4767,4772,4777,4781,4786,4791,4796,4801,4805,4810,4815,4819,4824,4829,4833,4838,4843,4848,4852,4857,4862,4867,4871,4876,4881],{"__ignoreMap":28},[33,4763,4764],{"class":35,"line":36},[33,4765,4766],{"class":39},"# ── bats tests are in .bats files ──\n",[33,4768,4769],{"class":35,"line":43},[33,4770,4771],{"class":39},"# bats test\u002F runs all *.bats files. .sh test files are ignored.\n",[33,4773,4774],{"class":35,"line":60},[33,4775,4776],{"class":39},"# Name test files: test\u002Ffeature_test.bats\n",[33,4778,4779],{"class":35,"line":78},[33,4780,137],{"emptyLinePlaceholder":136},[33,4782,4783],{"class":35,"line":91},[33,4784,4785],{"class":39},"# ── `source` in a test runs in the test's shell ──\n",[33,4787,4788],{"class":35,"line":97},[33,4789,4790],{"class":39},"# Functions\u002Fvariables from the sourced file are available.\n",[33,4792,4793],{"class":35,"line":109},[33,4794,4795],{"class":39},"# But `set -e` in the sourced file applies to the test shell (may cause unexpected exits).\n",[33,4797,4798],{"class":35,"line":133},[33,4799,4800],{"class":39},"# Don't use `set -e` in libraries — use it only in scripts.\n",[33,4802,4803],{"class":35,"line":140},[33,4804,137],{"emptyLinePlaceholder":136},[33,4806,4807],{"class":35,"line":146},[33,4808,4809],{"class":39},"# ── `run` captures stdout AND stderr ──\n",[33,4811,4812],{"class":35,"line":159},[33,4813,4814],{"class":39},"# $output has both. Use `run --separate-stderr` (bats 1.5+) for $stderr separately.\n",[33,4816,4817],{"class":35,"line":172},[33,4818,137],{"emptyLinePlaceholder":136},[33,4820,4821],{"class":35,"line":177},[33,4822,4823],{"class":39},"# ── Mocking with functions affects the test shell ──\n",[33,4825,4826],{"class":35,"line":183},[33,4827,4828],{"class":39},"# `git() { ... }` overrides git in the test. Always unset in teardown: `unset -f git`.\n",[33,4830,4831],{"class":35,"line":189},[33,4832,137],{"emptyLinePlaceholder":136},[33,4834,4835],{"class":35,"line":195},[33,4836,4837],{"class":39},"# ── `$BATS_TEST_FILENAME` vs `$0` ──\n",[33,4839,4840],{"class":35,"line":200},[33,4841,4842],{"class":39},"# $0 is the bats executable. Use $BATS_TEST_FILENAME for the test file's path.\n",[33,4844,4845],{"class":35,"line":206},[33,4846,4847],{"class":39},"# source \"$(dirname \"$BATS_TEST_FILENAME\")\u002F..\u002Flib\u002Futils.sh\"\n",[33,4849,4850],{"class":35,"line":215},[33,4851,137],{"emptyLinePlaceholder":136},[33,4853,4854],{"class":35,"line":221},[33,4855,4856],{"class":39},"# ── bats and `set -e` ──\n",[33,4858,4859],{"class":35,"line":243},[33,4860,4861],{"class":39},"# A failing `[[ ]]` in a @test causes the test to fail (good).\n",[33,4863,4864],{"class":35,"line":263},[33,4865,4866],{"class":39},"# But `set -e` in a sourced file can exit the test shell. Don't source files with `set -e`.\n",[33,4868,4869],{"class":35,"line":280},[33,4870,137],{"emptyLinePlaceholder":136},[33,4872,4873],{"class":35,"line":286},[33,4874,4875],{"class":39},"# ── Tests don't catch everything ──\n",[33,4877,4878],{"class":35,"line":291},[33,4879,4880],{"class":39},"# bats tests behavior (runtime). ShellCheck catches static issues (quoting, unset vars).\n",[33,4882,4883],{"class":35,"line":299},[33,4884,4885],{"class":39},"# Use BOTH: bats for behavior, ShellCheck for static analysis.\n",[15,4887,4889],{"id":4888},"quick-quiz","🧠 Quick Quiz",[4891,4892,4893],"p",{},"Why does this test pass even when the function is broken?",[20,4895,4896],{"language":22},[24,4897,4899],{"className":26,"code":4898,"language":22,"meta":28,"style":28},"@test \"add works\" {\n    run add 2 3\n    [[ \"$(add 2 3)\" == \"5\" ]]\n}\n",[30,4900,4901,4910,4920,4940],{"__ignoreMap":28},[33,4902,4903,4905,4908],{"class":35,"line":36},[33,4904,370],{"class":46},[33,4906,4907],{"class":50}," \"add works\"",[33,4909,376],{"class":50},[33,4911,4912,4914,4916,4918],{"class":35,"line":43},[33,4913,636],{"class":46},[33,4915,3406],{"class":50},[33,4917,1209],{"class":68},[33,4919,1570],{"class":68},[33,4921,4922,4924,4926,4928,4930,4932,4934,4936,4938],{"class":35,"line":60},[33,4923,308],{"class":117},[33,4925,548],{"class":50},[33,4927,1159],{"class":46},[33,4929,1209],{"class":68},[33,4931,800],{"class":68},[33,4933,1416],{"class":50},[33,4935,478],{"class":227},[33,4937,1421],{"class":50},[33,4939,413],{"class":117},[33,4941,4942],{"class":35,"line":78},[33,4943,283],{"class":117},[4945,4946,4947,4951,4963,4993,5009,5041,5047,5089,5092,5143],"details",{},[4948,4949,4950],"summary",{},"Answer",[4891,4952,4953,4954,4957,4958,4962],{},"The test calls ",[30,4955,4956],{},"add 2 3"," ",[4959,4960,4961],"strong",{},"twice",":",[4964,4965,4966,4983],"ol",{},[4967,4968,4969,4972,4973,4975,4976,4978,4979,4982],"li",{},[30,4970,4971],{},"run add 2 3"," — captures into ",[30,4974,579],{},"\u002F",[30,4977,582],{},", but ",[4959,4980,4981],{},"neither is checked",".",[4967,4984,4985,4988,4989,4992],{},[30,4986,4987],{},"$(add 2 3)"," — the ",[30,4990,4991],{},"[[ ]]"," checks this call's output against \"5\".",[4891,4994,4995,4996,4998,4999,5001,5002,5004,5005,5008],{},"If ",[30,4997,1159],{}," is broken (returns \"6\"), the ",[30,5000,575],{}," call's output (\"6\") is ignored, and the ",[30,5003,4991],{}," fails — the test ",[4959,5006,5007],{},"does"," fail. But the issue is:",[5010,5011,5012,5020,5029],"ul",{},[4967,5013,5014,3606,5017,5019],{},[4959,5015,5016],{},"Wasteful",[30,5018,1159],{}," runs twice.",[4967,5021,5022,5025,5026,5028],{},[4959,5023,5024],{},"Side effects",": if ",[30,5027,1159],{}," writes to a file or increments a counter, it runs twice.",[4967,5030,5031,5034,5035,5037,5038,5040],{},[4959,5032,5033],{},"Confusing",": the ",[30,5036,575],{}," is dead code (its output isn't checked). A reader might think the test checks ",[30,5039,575],{},"'s output.",[4891,5042,5043,5046],{},[4959,5044,5045],{},"The fix",": use one approach:",[20,5048,5049],{"language":22},[24,5050,5052],{"className":26,"code":5051,"language":22,"meta":28,"style":28},"@test \"add works\" {\n    run add 2 3\n    assert_success\n    assert_output \"5\"    # checks the run's output (single call)\n}\n",[30,5053,5054,5062,5072,5076,5085],{"__ignoreMap":28},[33,5055,5056,5058,5060],{"class":35,"line":36},[33,5057,370],{"class":46},[33,5059,4907],{"class":50},[33,5061,376],{"class":50},[33,5063,5064,5066,5068,5070],{"class":35,"line":43},[33,5065,636],{"class":46},[33,5067,3406],{"class":50},[33,5069,1209],{"class":68},[33,5071,1570],{"class":68},[33,5073,5074],{"class":35,"line":60},[33,5075,2316],{"class":46},[33,5077,5078,5080,5082],{"class":35,"line":78},[33,5079,1929],{"class":46},[33,5081,1421],{"class":50},[33,5083,5084],{"class":39},"    # checks the run's output (single call)\n",[33,5086,5087],{"class":35,"line":91},[33,5088,283],{"class":117},[4891,5090,5091],{},"Or (for pure functions):",[20,5093,5094],{"language":22},[24,5095,5097],{"className":26,"code":5096,"language":22,"meta":28,"style":28},"@test \"add works\" {\n    result=$(add 2 3)\n    [[ \"$result\" == \"5\" ]]\n}\n",[30,5098,5099,5107,5123,5139],{"__ignoreMap":28},[33,5100,5101,5103,5105],{"class":35,"line":36},[33,5102,370],{"class":46},[33,5104,4907],{"class":50},[33,5106,376],{"class":50},[33,5108,5109,5111,5113,5115,5117,5119,5121],{"class":35,"line":43},[33,5110,439],{"class":117},[33,5112,228],{"class":227},[33,5114,231],{"class":117},[33,5116,1159],{"class":46},[33,5118,1209],{"class":68},[33,5120,800],{"class":68},[33,5122,240],{"class":117},[33,5124,5125,5127,5129,5131,5133,5135,5137],{"class":35,"line":60},[33,5126,308],{"class":117},[33,5128,318],{"class":50},[33,5130,473],{"class":117},[33,5132,318],{"class":50},[33,5134,478],{"class":227},[33,5136,1421],{"class":50},[33,5138,413],{"class":117},[33,5140,5141],{"class":35,"line":78},[33,5142,283],{"class":117},[4891,5144,5145,5148,5149,5151,5152,5155,5156,1174,5158,5160,5161,1174,5164,5166],{},[4959,5146,5147],{},"The lesson",": don't mix ",[30,5150,575],{}," and direct ",[30,5153,5154],{},"$(...)"," calls in the same test. Pick one — ",[30,5157,575],{},[30,5159,582],{}," for commands, or ",[30,5162,5163],{},"$()",[30,5165,4991],{}," for pure functions.",[5168,5169,5170],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}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);}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}",{"title":28,"searchDepth":43,"depth":43,"links":5172},[5173,5174,5176,5177,5178,5179,5180,5181,5182,5183,5184,5185,5186],{"id":17,"depth":43,"text":18},{"id":572,"depth":43,"text":5175},"run, $status, $output: Testing Commands",{"id":1141,"depth":43,"text":1142},{"id":1766,"depth":43,"text":1767},{"id":2153,"depth":43,"text":2154},{"id":2636,"depth":43,"text":2637},{"id":3064,"depth":43,"text":3065},{"id":3378,"depth":43,"text":3379},{"id":3582,"depth":43,"text":3583},{"id":3858,"depth":43,"text":3859},{"id":4316,"depth":43,"text":4317},{"id":4753,"depth":43,"text":4754},{"id":4888,"depth":43,"text":4889},"Deep-dive into Bash testing with bats-core: test structure, run\u002F$status\u002F$output mechanics, bats-assert, setup\u002Fteardown lifecycle, function mocking with export -f, parameterized tests, and CI integration with ShellCheck. Code-first reference for senior engineers.","md",{},"\u002Fbash\u002F14-testing",{"title":5,"description":5187},"bash\u002F14-testing","zzmI1bhSTvDGr7Wg_EHKPOlazgZvYDTM2DVp78CMbh0",1789924644818]