Chương 3 Phương pháp xác định vị trí lỗi và gợi ý sửa lỗi
3.2. Xác định vị trí lỗi
Xác định vị trí lỗi (định vị lỗi) là bước đầu tiên trong quá trình sửa lỗi. Hiện có rất nhiều phương pháp định vị lỗi như đã trình bày trong Chương 2, phương pháp định vị lỗi quang phổ được lựa chọn để giải quyết bài toán vì tính hiệu quả và phù hợp của nó. Luận văn mô tả việc sử dụng thư viện mã nguồn mở Gzoltar [18] để thực thi định vị lỗi. Gzoltar sử dụng công thức tính quang phổ Ochiai và cung cấp các phương thức giúp định vị lỗi đến từng dòng lệnh. Gzoltar thường được tích hợp trong các công cụ sửa lỗi tự động. Trong phạm vi bài toán, việc sử dụng Gzoltar sẽ điều chỉnh để hiển thị kết quả phù hợp với mục tiêu gợi ý học sinh.
3.2.1. Xác định vị trí lỗi bằng phương pháp quang phổ
Tiền xử lí
Mục đích của bước tiền xử lí là biên dịch mã nguồn từ các tệp .java sang các tệp .class. Việc biên dịch sẽ được thực hiện bằng các câu lệnh Maven của Java mà không cần bổ sung thêm công cụ nào khác. Kết quả là các tệp mã nguồn và ca kiểm thử ở dạng nhị phân. Đây là các tham số đầu vào cho bước định vị lỗi tiếp theo. Hình 3.2 mô tả các bước tiền xử lí mã nguồn của các chương trình Java.
Hình 3.2. Tiền xử lí mã nguồn
Xác định vị trí lỗi bằng phương pháp quang phổ
Mô-đun nhận đầu vào là chương trình nguồn bị lỗi cùng với các bộ các ca kiểm thử tương ứng. Từ bộ mã nguồn và bộ ca kiểm thử ta sẽ tìm ra độ phủ của từng ca kiểm thử lên chương trình (Get Code Coverage). Quá trình này được hiểu là tính toán với mỗi câu lệnh tương ứng trong chương trình có những ca kiểm thử nào đi qua. Kết thúc quá trình xác định độ phủ ta có danh sách sách các câu lệnh mà mỗi ca kiểm thử đi qua. Kết quả trên là đầu vào cho công thức quang phổ (Spectrum Fomular). Công thức quang phổ tính toán và đưa ra điểm số nghi ngờ của từng câu lệnh trong chương trình (Suspicious Score list). Điểm nghi ngờ càng cao thì khả năng có lỗi ở câu lệnh càng lớn. Mô tả luồng xử lí của thuật toán định vị lỗi quang phổ như Hình 3.3.
Bộ mã nguồn (.Java) Biên dịch Danh sách các gói (.class) Danh sách các ca kiểm thử (.class)
15
Bộ ca kiểm thử của chương trình bao gồm hai loại ca kiểm thử, ca kiểm thử làm cho chương trình bị lỗi gọi là ca kiểm thử tiêu cực (Negative Test - NT) và ca kiểm thử mà chương trình vượt qua được gọi là ca kiểm thử tích cực (Positive Test – PT). Khi chương trình chạy qua bộ ca kiểm thử, độ phủ từng ca kiểm thử được ghi lại và đưa vào tính toán theo công thức quang phổ. Các công thức tính quang phổ đã được trình bày trong Chương 2, luận văn tìm hiểu và trình bày công thức tính quang phổ Ochiai [18] (Công thức 3.1). Công thức Ochiai giúp tính toán điểm nghi ngờ cho từng câu lệnh. Trong đó, NF là số lượng Negative Test, NCF là số lượng Negative Test thực thi trên một dòng lệnh, NCS là số lượng Positive Test thực thi trên một dòng lệnh. Điểm nghi ngờ cho một câu lệnh được tính bằng tỉ lệ giữa số lượng NT thực thi trên dòng lệnh đó và tích giữa số lượng NT có trong bộ ca kiểm thử nhân với tổng số ca kiểm thử chạy qua câu lệnh đó (gồm cả NT và PT).
𝑆𝑢𝑠𝑝𝑖𝑐𝑖𝑜𝑢𝑠𝑛𝑒𝑠𝑠 (𝑂𝑐ℎ𝑖𝑎𝑖) = 𝑁𝐶𝐹
√𝑁𝐹 × (𝑁𝐶𝐹 + 𝑁𝐶𝑆)
Để minh họa cách tính điểm nghi ngờ theo công thức Ochiai ta đi xem xét ví dụ mã nguồn lỗi (Bảng 3.1). Có năm ca kiểm thử, trong đó có bốn ca kiểm thử PT tương ứng trạng thái test outcome là Pass (P), một ca kiểm thử NT tương ứng trạng thái test outcome là Fail (F). Các dấu x thể hiện câu lệnh nằm trên đường thực thi của ca kiểm thử đó (các dấu x theo chiều ngang của một câu lệnh thể hiện các ca kiểm thử đi qua câu lệnh đó). Ta thấy, câu lệnh 1 có bốn ca kiểm thử PT và một ca kiểm thử NT đi qua, câu lệnh 2 có một ca kiểm thử NT và một ca kiểm thử PT đi qua, câu lệnh 4 có ba ca kiểm thử PT đi qua, tương tự cho các câu lệnh khác trong chương trình.
Áp dụng công thức tính Ochiai để tính điểm nghi ngờ cho từng câu lệnh. Với câu lệnh 2 thì Suspiciousness (1) = 1/(sqrt(1*(1+1)) = 0.7. Làm tương tự với những câu lệnh khác ta được kết quả như Bảng 3.2. Kết quả này cho thấy, ở các dòng 4 đến 11 điểm nghi ngờ bằng 0 có nghĩa là các câu lệnh này không chứa lỗi. Dòng 1 điểm nghi ngờ 0.4, dòng 2 có điểm nghi ngờ 0.7 là chỉ số cao nhất, điều này đồng nghĩa với việc dòng 2 là dòng có khả năng gây lỗi lớn nhất. Thuật toán gcd trong ví dụ để tìm ước chung lớn nhất của hai số a và b. Trong trường hợp a = 0 (câu lệnh 1), thì kết quả trả về ước chung
Tính độ bao phủ
từng câu lệnh Công thức quang phổ
Danh sách câu lệnh và điểm nghi ngờ tương ứng
Các ca kiểm thử Mã nguồn lỗi
Hình 3.3. Luồng xử lí của thuật toán định vị lỗi quang phổ
16
lớn nhất phải bằng b (return b), nhưng câu lệnh 2 lại trả lại kết quả của hàm là a (return a). Vậy thuật toán này sai, sai ở chính vị trí câu lệnh 2.
Với kết quả chỉ lỗi như Bảng 3.2, người dùng căn cứ vào điểm nghi ngờ từng câu lệnh để sửa chữa chương trình. Những câu lệnh có điểm nghi ngờ lớn sẽ được tập trung xem xét và sửa chữa trước thay vì tìm kiếm lỗi trong toàn bộ chương trình. Bước này có thể thực hiện lại nhiều lần cho tới khi nhận được kết quả đúng. Hoặc nếu vẫn chưa đúng có thể chuyển sang lựa chọn thứ hai, xem gợi ý chương trình như Mục 3.3.
Code Test case
public static int gcd(int a, int b){ (0,0) (0,10) (10,0) (10,15) (15,10)
1 if (a ==0) { x x x x x 2 return a; x x 3 } 4 while (b != 0) { x x x 5 if (a > b) x x 6 a = a - b; x x 7 else x x 8 b = b - a; x x 9 } 10 return a; x x x 11 } Test outcome P F P P P Bảng 3.1. Ví dụ về mã nguồn và ca kiểm thử Code Điểm nghi ngờ
public static int gcd(int a, int b) {
1 if (a ==0) { 0.4 2 return a; 0.7 3 } 0 4 while (b != 0) { 0 5 if (a > b) 0 6 a = a - b; 0 7 else 0 8 b = b - a; 0 9 } 0 10 return a; 0 11 } 0 Bảng 3.2. Kết quả chỉ lỗi
17
3.2.2. Sử dụng thư viện Gzoltar để xác định vị trí lỗi
Bước xác định vị trí lỗi trong bài toán sử dụng thư viện Gzoltar, thư viện này tích hợp công thức tính quang phổ Ochiai. Luồng xử lí định vị lỗi bằng Gzoltar được mô tả bởi Hình 3.4. Bước tiền xử lí đã được mô tả trong Mục 3.2.1 cho kết quả là các tệp tin đã được biên dịch. Đầu vào cho công cụ Gzoltar bao gồm tên các gói mã nguồn lỗi, tên các tệp mã nguồn chứa ca kiểm thử và đường dẫn đến thư mục chứa các tệp mã nguồn sau khi nó được biên dịch.
Hình 3.4. Luồng xử lí định vị trí lỗi
Để đưa các tệp tin .class vào Gzoltar, ta sử dụng các API có sẵn. Mã nguồn 3.1 dưới đây mô tả cách cấu hình các thuộc tính cho Gzoltar. Đầu tiên cần cấu hình đường dẫn tới thư mục chứa các tệp tin class cần kiểm tra lỗi (pathToClassSource) – dòng lệnh 4. Dòng lệnh 6 khởi tạo đối tượng Gzoltar tên gzoltar với tham số đầu vào là đường dẫn pathToClassSource được khai báo ở tên. Vì có nhiều chương trình tương ứng với nhiều bài toán khác nhau, ta cần cung cấp tên class đầu vào cần định vị lỗi bằng phương thức addClassToInstrument() – dòng lệnh 7. Tương tự, dòng lệnh 8 thêm các ca kiểm thử vào để kiểm tra mã nguồn lỗi bằng phương thức addTestToExecute(). Dòng lệnh 9 gọi tới phươngthức run() thực thi việc tính toán điểm nghi ngờ từng câu lệnh theo công thức Ochiai đã được cài đặt sẵn.
Cuối cùng, để lấy ra kết quả điểm nghi ngờ từng câu lệnh dùng phương thức getSuspiciousStatements(). Phương thức này cho phép lấy ra tên class lỗi - getLabel(), dòng lệnh - getLineNumber() cùng với chỉ số nghi ngờ của nó -
Bộ mã nguồn (.java) Biên dịch Danh sách các gói (.class) Danh sách các ca kiểm thử (.class) Gzoltar
18
getSuspiciousness() và hiển thị dưới dạng một chuỗi, ví dụ soNguyenTo.problem1.Problem1@10@0.816 – là kết quả tính điểm nghi ngờ class problem1 của chương trình soNguyenTo tại dòng 10 bằng 0.816. Mỗi class bao gồm nhiều dòng lệnh nên kết quả điểm nghi ngờ mỗi dòng lệnh hiển thị tương tự như trên, hàm for (dòng 10 đến 12) giúp ta thực hiện việc này.
Mã nguồn 3.1. Sử dụng Gzoltar để định vị lỗi 3.3. Gợi ý sửa lỗi chương trình 3.3. Gợi ý sửa lỗi chương trình
3.3.1. Tổng quan về giải pháp gợi ý chương trình
Sau khi được chỉ vị trí lỗi học sinh sẽ tiến hành sửa lỗi. Nếu chương trình vẫn chưa đúng sau khi sửa lỗi, học sinh lựa chọn xem gợi ý sửa lỗi thì kích hoạt mô-đun gợi ý sửa lỗi. Bản chất của việc đưa ra gợi ý sửa lỗi chương trình là tìm chương trình đúng có mức độ tương đồng (tỉ lệ trùng lắp) cao, bài toán này tương đương với bài toán kiểm tra đạo văn, trong đó cũng xác định tỉ lệ trùng lắp giữa các chương trình xem tỉ lệ đó có vượt ngưỡng cho phép hay không. Chương 2 đã trình bày về các công cụ kiểm tra đạo văn để đưa ra lựa chọn tích hợp công cụ JPlag vào giải quyết bài toán. Quy trình thực thi các bước trong JPlag được trình bày trong mục này có tham khảo nội dung trong báo cáo phát hiện tương đồng của công cụ JPlag [24].
Quy trình gợi ý sửa lỗi chương trình sử dụng JPlag được mô tả bởi Hình 3.5. Đầu vào của mô-đun là chương trình lỗi của học sinh và các chương trình đúng khác đã được lưu trong bộ sưu tập. Quy trình thực hiện bao gồm các bước:
19
Bước thứ hai: So sánh các chuỗi Tokens của các cặp chương trình, tính ra tỉ lệ tương đồng cao nhất của các chuỗi Tokens ở các cặp.
Bước cuối: Hiển thị kết quả so sánh tại bước 2 làm gợi ý sửa lỗi cho người dùng.
Hình 3.5. Quy trình thực thi của JPlag 3.3.2. Phân tích chương trình thành chuỗi các Tokens 3.3.2. Phân tích chương trình thành chuỗi các Tokens
Các chương trình cần so sánh được phân tích thành các thẻ Tokens, việc này được hiểu là chuyển các dòng lệnh sang dạng các mã thông báo, các mã thông báo là căn cứ để so sánh ở bước sau. Mã thông báo đặc trưng cho bản chất của câu lệnh chứ không phụ thuộc vào hình thức thể hiện của tên biến, tên hàm. Ngoài ra JPlag cũng quy định các lời gọi phương thức, các ngoại lệ, dòng chú thích hay các dòng trắng sẽ không được gắn mã thông báo, do đây là điểm dễ thay đổi mà không ảnh hưởng đến chương trình. Danh sách các mã thông báo đầy đủ trong phụ lục mã Tokens của công cụ JPlag tại phần cuối của luận văn.
Để hiểu về cách phân tích chượng trình thành các mã thông báo ta xem xét ví dụ Mã nguồn 3.2. Dòng 1, 4, 18, 19, 20 là các dòng chú thích, không được gắn mã thông báo. Dòng 17 là dòng trống, dòng 22 là ngoại lệ cũng bị bỏ qua không gắn mã. Các dòng còn lại được chuyển thành các mã thông báo (sau đây gọi là các Tokens) phản ánh đúng ý nghĩa của câu lệnh. Các dòng lệnh bắt đầu và kết thúc class, hàm, vòng lặp, khởi tạo một đối tượng được chuyển thành các Tokens đặc trưng với BEGINCLASS, BEGINMENTHOD, BEGINWHILE, BEGINIF và ENDCLASS,ENDMENTHOD, ENDWHILE, ENDIF. Dòng 3 khởi tạo lớp được chuyển thành Token BEGINCLASS. Dòng 5 khởi tạo một phượng thức và khai báo 2 biến đầu vào nên được chuyển thành 2 Tokens VARDEF và Token BEGINMENTHOD. Dòng lệnh 6, 7 là bắt đầu câu lệnh while và if được chuyển thành Tokens BEGINWHILE và BEGINIF. Các câu lệnh 8 và 10 là câu lệnh gán giá trị cho biến, vì vậy dù là 2 câu lệnh khác nhau nhưng đều được chuyển thành Token ASSIGN. Tượng tự với những câu lệnh khác.
20
Code Tokens
1 //import your library
2 import java.util.Scanner; IMPORT
3 public class Solution { BEGINCLASS
4 //Type your main code here
5 public static int gcd(int a, int b){ VARDEF, VARDEF,
BEGINMENTHOD
6 if (a==0) BEGINIF
7 return b; RETURN, ENDIF
8 while (a != b) { BEGINWHILE 9 if (a > b) { BEGINIF 10 a -= b; ASSIGN 11 } else { ELSE 12 b -= a; ASSIGN 13 } ENDIF 14 } ENDWWHILE 15 return a; RETURN 16 } ENDMENTHOD 17 18 /** 19 * Main. 20 */
21 public static void main(String[] args) VARDEF, BEGINMENTHOD
22 throws java.io.IOException {
23 int gcd; VARDEF
24 Scanner sc = newScanner(System.in); VARDEF,NEWCLASS,ASSIGN
25 int a = sc.nextInt(); VARDEF, ASSIGN
26 int b = sc.nextInt(); VARDEF, ASSIGN
27 gcd = gcd(a,b); ASSIGN
28 System.out.println(gcd); APPLY
29 } ENDMENTHOD
30 } ENDCLASS
Mã nguồn 3.2. Tách mã nguồn thành các Tokens
Chuyển chương trình thành các Tokens có ý nghĩa lớn trong khâu so sánh tại bước tiếp theo của bài toán này. Việc so sánh kiểu từ với từ giữa các chương trình để tìm chương trình tương đương là không khả thi. Bởi vì học sinh ở giai đoạn mới lập trình thường được yêu cầu sử dụng các chú thích để giải thích đoạn lệnh nên chương trình của mỗi học sinh có nhiều chú thích khác nhau. Ngoài ra do học sinh chưa hoàn thiện thói quen định dạng code nên các lệnh có sử dụng mở đóng ngoặc có thể bị dịch xuống nhiều dòng, thừa dòng trắng giữa các câu lệnh. Việc chuyển mã chương trình
21
thành các mã thông báo giúp giải quyết các vấn đề trên, cho phép tìm ra các chương trình đúng có độ tương tự tốt nhất với chương trình đang cần sửa chữa.
3.3.3. So sánh các chuỗi Tokens
Đầu vào của mô-đun gợi ý chương trình là chương trình lỗi của học sinh và rất nhiều chương trình mẫu khác được lưu trong bộ sưu tập. Vì vậy, việc so sánh được hiểu là so sánh chương trình lỗi với tất cả các chương trình mẫu trong bộ sưu tập. Để thực hiện việc này, chương trình lỗi sẽ so sánh lần lượt với từng chương trình mẫu (Hình 3.6). Với mỗi bộ chương trình lỗi – chương trình mẫu sẽ tính ra tỉ lệ tương đồng, kết quả này được hiển thị cho người dùng là căn cứ lựa chọn chương trình mẫu nào làm gợi ý.
Bước phân tích chương trình thành chuỗi các Tokens như mô tả ở Mục 3.3.2 cho kết quả mỗi chương trình được phân tách thành nhiều Tokens khác nhau, ta gọi là chuỗi các Tokens. So sánh hai chuỗi Tokens trong JPlag sử dụng thuật toán Greedy-String- Tiling (GST) [30] được phát triển bởi Michael Wise, sau đó các tác giả đã cải tiến bằng cách kết hợp với thuật toán Karp-Rabin để nâng cao hiệu quả.
So sánh hai chuỗi Tokens A và B, mục đích là tìm ra chuỗi con chung lớn nhất của hai mã nguồn từ đó tính ra tỉ lệ trùng lắp so với tổng thể chương trình. Chuỗi con này phải đảm bảo nguyên tắc:
i. Một Token trong A chỉ được so khớp với một Token trong B
ii. Các chuỗi con được tìm thấy phải độc lập với vị trí của chúng trong chuỗi. Hay việc thay đổi vị trí các thành phần trong mã nguồn chương trình không làm ảnh hưởng tới việc so sánh.
Chương trình lỗi Chương trình mẫu 1 . . . . Chương trình mẫu n Chương trình mẫu 2 So sánh So sánh So sánh
22
1 GSTiling (String A, String B) {
2 tiles { } do
3 maxmatch MinimumMatchLength
4 matches { }
5 for unmarkerd tokens Aa in A do 6 for unmarkerd tokens Bb in B do 7 j 0
8 while (Aa+j == Bb+j ) ∩ unmarkedAa+j ∩ unmarked Bb+j do
9 j++
10 end
11 if (j == maxmatch) then
12 matches matches match (a; b; j)
13 elseif j> maxmatch then
14 matches match (a; b; j) }
15 maxmatch j
16 end 17 end
18 for match(a; b; maxmatch) ∈ matches do
19 for j = 0 … (maxmatch -1) do 20 mark (Aa+j);
21 mark (Bb+j); 22 end
23 Tiles tiles ∪ match (a; b; maxmatch)
24 end
25 while (maxmatch > MinimumMatchLengt);
26 return tiles;
Thuật toán 3.1. Thuật toán Greedy-String-Tiling (GST) [30]
Thuật toán GST (Thuật toán 3.1) có đầu vào là hai chuỗi Tokens A, B. Thuật toán