我爲 Windows 10 修復了一個 bug

本文講述了一名開發者爲 Windows 計算器應用修復 bug 的經歷。

據這名開發者(下用 Peter 代稱)介紹,他某日在 Reddit 閒逛時,一個位於 Windows 10 子版塊下的帖子引起了他的注意。帖子內容如下:

和大家一樣,在計算兩個日期之間的相隔天數時,Peter 也發現了關於週數的描述明顯是錯誤的,如此大的數值看起來應該是上溢或者下溢之類的問題,要不就是差一錯誤(off-by-one)等常見的邏輯錯誤。

本着對這個 bug 的好奇心,再加上 Windows 10 計算器是開源項目,Peter 認爲解決這個問題應該不會太複雜,所以他希望親自找到 bug 並進行修復。

他先在自己的電腦上測試看是否能復現,按照帖子的示例,在測試 7.31-12.31 的間隔天數時,計算器返回的結果是正確的 —— “5個月”。接着 Peter 稍微改了一下日期,改成 7.31-12.30 時,bug 復現了,計算器顯示的值爲:“5 months, 613566756 weeks, 3 days”,這明顯是錯誤的。

確定了 bug 的存在,Peter 決定從 Windows 計算器的 GitHub 倉庫下載源碼來研究一番。從 repo 把源碼下載到本地後,由於在 IDE 運行 Windows 計算器項目需要 UWP workload,所以 Peter 還爲 Visual Studio 添加了 UWP workload。不過 Peter 表示搭建開發環境也十分順利,修 bug 第一步至此完成。

接着 Peter 打開了解決方案文件(solution file),查看 “Calculator” 項目,並搜索看似相關的任何文件。他找到了界面文件DateCalculator.xaml,接着從相關的文件DateDiff_FromDate追蹤到了DateCalculatorViewModel.cpp,最後找到DateCalculator.cpp

然後 Peter 開始設置斷點並觀察相關變量的變化,他發現 final 變量DateDifference 的值有誤。因此他判斷這個 bug 不是由轉換爲字符串存在錯誤而導致的,而是實實在在的計算錯誤。

既然計算存在問題,那就看看它的計算邏輯是如何實現的。

Windows 計算器對間隔日期的計算邏輯用僞代碼表示如下:

DateDifference calculate_difference(start_date, end_date) {     uint[] diff_types = [year, month, week, day]     uint[] typical_days_in_type = [365, 31, 7, 1]     uint[] calculated_difference = [0, 0, 0, 0]     date temp_pivot_date     date pivot_date = start_date     uint days_diff = calculate_days_difference(start_date, end_date)      for(type in differenceTypes) {         temp_pivot_date = pivot_date         uint current_guess = days_diff /typicalDaysInType[type]          if(current_guess !=0)             pivot_date = advance_date_by(pivot_date, type, current_guess)                  int diff_remaining         bool best_guess_hit = false         do{             diff_remaining = calculate_days_difference(pivot_date, end_date)             if(diff_remaining < 0) {                 // pivotDate has gone over the end date; start from the beginning of this unit                 current_guess = current_guess - 1                 pivot_date = temp_pivot_date                 pivot_date = advance_date_by(pivot_date, type, current_guess)                 best_guess_hit = true             } else if(diff_remaining > 0) {                 // pivot_date is still below the end date                 if(best_guess_hit)                     break;                 current_guess = current_guess + 1                 pivot_date = advance_date_by(pivot_date, type, 1)             }         } while(diff_remaining!=0)          temp_pivot_date = advance_date_by(temp_pivot_date, type, current_guess)         pivot_date = temp_pivot_date          calculated_difference[type] = current_guess         days_diff = calculate_days_difference(pivot_date, end_date)     }     calculcated_difference[day] = days_diff     return calculcated_difference } 

上面的代碼主要做了這些事:先算出相差的年數、然後計算相差的月數、接着計算相差的週數、最後計算相差的天數。

Peter 表示這看起來很正常,他沒發現其中的邏輯存在錯誤。

問題正是在於此,寫這段代碼的人以爲代碼會按預料中執行:

date = advance_date_by(date, month, somenumber) date = advance_date_by(date, month, 1) 

逐一運行後如下:

date = advance_date_by(date, month, somenumber + 1)  

常見情況下的確如此。

但問題在於:“如果起始日期爲某月的第 31 天,結束日期所在的月份只有 30 天,該以哪天作爲結束的標誌?”對於 Windows.Globalization.Calendar.AddMonths(Int32) 來說,它的答案顯然是“在第 30 天”。

具體而言,這就意味着:

“July 31st + 4 Months = November 30th” “November 30th + 1 Month = December 30th”

然而實際情況是:

“July 31st + 5 Months = December 31st”

這就引起了“差一錯誤”。逐步調用Window::Globalization::Calendar::AddMonths會導致GetDifferenceInDays出現負值,然後將其分配給無符號變量daysDiff,經過後面的循環迭代後,daysDiff會將這個負值變爲更大的數字。

接着 Peter 在 Windows 計算器的 GitHub 倉庫提交了一個 PR 以進行最小化“修復”。

date = advance_date_by(date, month, somenumber + 1)  

Peter 爲修復加上了引號,是因爲它最後計算出的結果如下:

Peter 表示,如果各位認可“7月31日+ 4個月= 11月30日”這樣的結果,他認爲這在技術上是正確的。雖然完整的結果不符合大衆對日期間隔天數的閱讀習慣,但至少不會出錯。

不過這件事中,最令人深刻的是微軟最後合併了 Peter 提交的 PR 以修復這個問題。

這說明微軟的開源項目不僅僅是將代碼託管在 GitHub 而已,而是會聽取來自社區用戶的建議和改進。

那麼問題來了,如果是你,你會怎樣解決這個錯誤呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章