前幾天一路在補文件,今天回到會動的東西:題庫。
起點是 Day 28 那條新寫下的限制——題庫會用完。出題改成從題庫挑之後,
10 題做完就沒得換了。
但打開來數才發現,我對問題的描述是錯的。
我原本以為「有些弱點根本沒題目可挑」。實際數完:
control-flow 8 io-parse 5
loop 7 io-eof 3
off-by-one 6 indentation 2
output-format 5 type-confusion 1
每一個代碼都至少有 1 題。 閉環沒有斷,只是很薄。
而薄的後果不一樣:shortlist() 一次取 6 個候選,學生做掉 5 題就開始見底;type-confusion 只有 1 題,代表這個弱點的「針對性練習」總共只有一次。
沒斷但很薄,跟斷掉,要用不同的方式修。 我差點照著錯的描述去修。
加題很單純,寫就是了。真正卡住我的是另一件事:
我怎麼知道我寫的測資是對的?
data/README.md 開頭就寫著測資錯是這個系統裡最貴的錯,那條傳染鏈我寫過三次了:
測資錯了
→ 評測照著錯的測資判(而且是「實際執行」,看起來非常確定)
→ summary 把錯的結果寫成一句話
→ 導師引用那句話去引導學生
→ 學生對著一份正確的程式碼,被要求找出不存在的錯
而在今天之前,這件事的全部保證是:我說我驗過了。
一句話,沒有任何東西撐著。這跟 Day 21 那條「判定是事實還是推測要明講」
是同一個毛病——只是這次不誠實的對象是我自己。
每一題附一份參考解答 data/solutions/<id>.py,測試把它丟進 sandbox.run_all
跑過該題全部測資,必須全過:
results = sandbox.run_all(path.read_text(encoding="utf-8"), problem["tests"])
failures = [... for i, r in enumerate(results) if not r.passed]
assert not failures
沒有新的機制,就是那個 Day 20 寫的執行器。它本來的工作是跑學生的程式,
現在多一份工作:跑我的程式,來證明我的測資。
有意思的是這兩件事共用同一條判定規則(problems.outputs_match())。
所以這支測試順便驗了一件別的事:如果比對規則哪天改壞了,題庫會整批變紅。
新題 own-001 的第二組測資,我手寫預期輸出:
1234567890123 × 2 = 2468135780246
跑出來是 2469135780246。
我手算錯了一位。
這個錯如果活到學生面前會發生什麼:他寫了一份完全正確的程式,被判「第 2 組
wrong-output」,導師接著引用這份判定開始引導他去找一個不存在的 bug。
他愈認真看自己的程式,愈找不到。而且系統的每一層都在說同一件事,
他沒有任何辦法發現是題目錯了。
一個手滑,一路走到底就是這樣。
寫測試的時候我當然不知道會抓到什麼——抓到的是自己十分鐘前的失誤,
這比抓到三個月前的舊 bug 更有說服力:這種錯不是偶爾發生的,是隨時在發生的。
"""**解答跑不過時不要改測資去配合解答,也不要反過來。**
兩邊都可能是錯的,要回去看題目本身。"""
紅燈的第一反應是「改哪邊比較快」。但這支測試比對的是兩份獨立的東西,
紅燈只說「它們不一致」,沒說誰錯。改哪一邊都能變綠,而其中一種會讓錯誤
從此有測試背書——比沒有測試更糟。
今天這次是我確認過 1234567890123 × 2 之後才改的測資,不是因為改測資比較快。
補完既有 10 題(UVa)的參考解答之後,第一次跑就全綠。
有點失望,因為我本來期待揪出一兩個舊測資的錯,那會是更好的文章素材。
但這件事本身是資訊:現有的題庫是自洽的,之前手工驗測資的那次沒有出包。
誠實記錄「這次沒抓到東西」跟記錄抓到東西一樣重要。一支從來沒紅過的測試
可能是沒必要,也可能是它守的東西剛好都還沒壞——差別在你有沒有看過它紅。
今天它紅過了(own-001),所以我知道它是會動的。
門檻定 3 不定 5。 覆蓋率測試要求每個 skill 至少 3 題。
定 5 的話,測試會逼我現在就生出更多題——而被測試逼出來的題目會是灌水的。
門檻要守的是「這個弱點挑得到不重複的題」,不是「題庫夠豐富」。
一個會逼人交垃圾的門檻,比沒有門檻更糟。
新題是原創題,不是 UVa 題。 我可以憑記憶寫 UVa 題的摘要與測資,
但我可能記錯輸入格式或邊界,而錯的測資沒人發現得了。原創題我自己出、
自己寫解答、跑過才入庫——用「可驗證」換掉「是真題」。
對這個系統來說,題目真不真實不重要,測資對不對是生死問題。
題庫 10 → 18 題,type-confusion 1 → 5、indentation 2 → 4。
測試 327 → 372 支全綠。