• Với mỗi bài toán, làm thế nào để:
– Thiết kế giải thuật nhằm giải quyết bài toán đó – Cài đặt giải thuật bằng một chương trình máy tính
- Hãy làm cho chương trình chạy đúng trước khi tăng tính hiệu quả của chương trình - Hãy tăng tính hiệu quả của chương trình và đồng thời thể hiện tốt phong cách lập trình của cá nhân
CHƯƠNG IV. CÁC KỸ THUẬT KIỂM TRA TÍNH ĐÚNG ĐẮN VÀ TÍNH AN TOÀN CỦA CHƯƠNG TRÌNH PHẦN MỀM
I. Bẫy lỗi (error handling) II. Lập trình phòng ngừa (defensive programming) III. Kiểm thử (Testing) IV. Gỡ rối (Debugging)
Mở đầu
• Lỗi: chương trình chạy không đúng như đã định • Chuỗi kiểm tra chương trình
• Phân biệt:
– Bẫy lỗi (error handling) – Lập trình phòng ngừa (defensive programming) – Kiểm thử (testing) – Gỡ rối (debugging)
– Bẫy lỗi: Prevent errors – Lập trình phòng ngừa: Detect problems as early as
– Kiểm thử: finished code – Gỡ rối: fixing defects uncovered by testing
possible
I. BẪY LỖI
Nguyên tắc
• Khi lỗi xảy ra cần
– Định vị nguồn gây lỗi – Kiểm soát lỗi
• Luôn có ý thức đề phòng các lỗi hay xảy ra trong
chương trình, nhất là khi đọc file, dữ liệu do người dùng nhập vào và cấp phát bộ nhớ.
• Áp dụng các biện pháp phòng ngừa ngay cả khi điều đó có thể dẫn tới việc dừng chương trình
• In các lỗi bằng stderr stream.
fprintf (stderr,"There is an error!\n");
Kiểm tra cái gì để phát hiện lỗi ?
• Kiểm tra mọi thao tác có thể gây lỗi khi viết CT
– Nhập dữ liệu
– Sử dụng dữ liệu
• Ví dụ:
– Kiểm tra mỗi lần mở một tệp tin hay cấp phát các ô nhớ.
– Kiểm tra các phương thức người dùng nhập dữ liệu vào cho
đến khi không còn nguy cơ gây ra dừng chương trình
– Trong trường hợp tràn bộ nhớ (out of memory), nên in ra
lỗi kết thúc chương trình (-1: error exit);
– Trong trường hợp dữ liệu do người dùng đưa vào bị lỗi, tạo cơ hội cho người dùng nhập lại dữ liệu (lỗi tên file cũng có thể do người dùng nhập sai)
Làm gì khi phát hiện lỗi ?
• Cần có cách xử lý các lỗi mà ta chờ đợi sẽ xảy ra • Tùy theo tình huống cụ thể, ta có thể
– Trả về 1 giá trị trung lập – Thay thế đoạn tiếp theo của dữ liệu hợp lệ – Trả về cùng giá trị như lần trước – Thay thế giá trị hợp lệ gần nhất – Ghi vết 1 cảnh báo vào tệp – Trả về 1 mã lỗi – Gọi 1 thủ tục hay đối tượng xử lý – Hiện thông báo lỗi – Tắt máy
Một số lỗi nhập dữ liệu phổ biến
• Dữ liệu nhập vào quá lớn (ví dụ, vượt quá kích
thước kích thước lưu trữ cho phép của mảng hay của biến)
• Dữ liệu nhập vào sai kiểu, giá trị quá nhỏ hoặc
giá trị âm
• Lỗi chia cho số 0 (divide by zero)
Dùng hàm bao gói (Wrappered function)
• Hàm bao gói = gọi hàm gốc + bẫy lỗi • Tại mọi thời điểm cần kiểm tra lỗi của hàm gốc, dùng hàm
bao gói thay vì dùng hàm gốc
• Ví dụ: • Nếu phải viết đoạn mã kiểm tra lỗi mỗi lần sử dụng malloc
thì rất nhàm chán. – Kiểm tra – Thông báo lỗi kiểu như “out of memory” – Thoát.
• Tự viết hàm safe_malloc và sử dụng hàm này thay vì dùng
malloc có sẵn – Malloc – Kiểm tra – Thông báo lỗi kiểu như “out of memory” – Thoát.
safe_malloc
“Không đủ bộ nhớ để thực hiện dòng lệnh :%d file :%s\n", __LINE__, __FILE__);
#include
#include
void *safe_malloc (size_t);
/* Bẫy lỗi khi dùng hàm malloc */
void *safe_malloc (size_t size)
/* Cấp phát bộ nhớ hoặc báo lỗi và thoát */
{
void *ptr;
ptr= malloc(size);
if (ptr == NULL) {
fprintf (stderr,
exit(-1);
}
return ptr;
}
Tạo giao diện rõ ràng
• Các LTV giỏi luôn tìm cách làm cho mã nguồn của họ
trở nên hữu dụng với những LTV khác.
• Cách tốt nhất để làm việc này là viết các hàm dễ hiểu,
có khả năng tái sử dụng
• Các hàm tốt không cần đến quá nhiều tham số truyền
vào
• Để viết tốt các hàm, cần tư duy theo hướng:
– Cần truyền cái gì vào để thực hiện hàm ? – Có thể lấy được cái gì ra sau khi thực hiện hàm
• Nếu LTV có khả năng viết được một giao diện rõ ràng
thì các hàm tự bản thân nó trở nên hiệu quả: – Các hàm được cung cấp – Cách thức truy nhập chức năng muốn cung cấp
Ví dụ: giao diện thể hiện được cấu trúc của chương trình
pay.h Header file - enums, structs, prototypes
pay.cpp #include "pay.h" int main() Lấy dữ liệu vào từ người dùng Gọi các chương trình con hợp lý
update.cpp #include "pay.h" Tạo file mới cho mỗi NV mới Ghi vào file đang có cho NV đã có tên
fileio.cpp #include "pay.h" Đọc các records khi được gọi Ghi vào các records Tạo bản sao
printout.cpp #include "pay.h" In bảng lương của tất cả các nhân viên In các bảng lương của từng nhân viên để đối chiếu
Đơn giản hóa các hàm bằng cách cấu trúc hóa chương trình
• Đôi khi cần truyền rất nhiều tham số vào một
hàm
Sẽ tốt hơn nếu xây dựng 1 struct và thao tác trên struct đó
void write_record (FILE *fptr, char name[], int wage, int hire_date, int increment_date, int pay_scale, char address[]) /* Hàm ghi thông tin liên quan đến 1 NV */ { } void write_record (FILE *fptr, EMPLOYEE this_employee) /* Hàm ghi thông tin liên quan đến 1 NV */ { }
Các hàm phải nhất quán (consistent)
• Nếu cần tạo ra loạt các hàm tương tự nhau, thì nên tổ chức mã nguồn của các hàm đó sao cho logic hay quy trình nghiệp vụ của các hàm đó là như nhau
Hàm thứ 2 khác hẳn hàm thứ nhất
Khi tái sử dụng, khả năng gây lỗi là rất cao
int write_record (char fname[],EMPLOYEE employee) /* Trả về -1 nếu ghi bị lỗi, trả về 0 nếu ghi thành công*/ { } int add_record (EMPLOYEE employee, char fname[]) /* Trả về 0 nếu lỗi, trả về 1 nếu thành công*/ { }
Không tùy tiện thay đổi cách thức hoạt động của hàm
• Chỉ thay đổi tham số của hàm nếu không còn lựa
chọn nào khác.
• Không thay đổi mục tiêu, nhiệm vụ của hàm: ví
dụ: nếu mục tiêu ban đầu không phải là cập nhật mảng, thì trong hàm đừng thực hiện việc cập nhật mảng
FILE *fptr; char fname[]= "file.txt"; fptr= fopen (fname, "r"); if (fptr == NULL) { printf ("Can't open %s\n",fname); return -1; }
Nếu fopen làm thay đổi không báo trước giá trị lưu trong fname, điều gì sẽ xảy ra?
Buffer Overflow & vấn đề truy nhập trái phép
• Hacker có thể khai thác các lỗi (bug) của hệ điều hành (Windows/Unix/Mac OS) hay phần mềm để truy nhập trái phép vào máy tính
• Một trong những lỗi được khai thác rất nhiều là "buffer
overflows".
• Có nhiều cách gây ra buffer overflows
– Sử dụng cẩu thả "strcpy" (sao chép 1 xâu lớn hơn vào 1
biến có kích thước nhỏ hơn). – Dùng lệnh gets (thư viện stdio) – Quên không kiểm tra độ lớn của xâu vào – ..
• Kiểm tra dữ liệu vào từ người dùng là một trong
những cách hạn chế truy nhập trái phép
Buffer Overflow
• Cái gì xảy ra nếu xâu lưu trong str2 dài hơn xâu
lưu trong str1?
void strcpy(char str1[], char str2[]) /* Copy xâu từ str2 vào str1 */ { int i= 0; while ((str1[i]= str2[i]) != '\0') i++; }
Another complex line which assigns and compares.
Buffer overflows
• Cái gì xảy ra nếu người dùng nhập vào 10 ký tự ?
100 ký tự ?
printf("Hello and Goodbye %s\n", name);
char name [25]; printf("Enter your name: "); fflush(stdout); if (gets(name) != NULL) return 0;
#include
safe-gets
• Dùng fgets() thay thế cho gets().
– fgets() không tự loại bỏ \n (new line) ở cuối xâu như
gets()
char *result = buffer, *np; if ((buffer == NULL) || (count < 1))
result = NULL;
if (np = strchr(buffer, '\n'))
*np = '\0';
else if ((result = fgets(buffer, count, stdin)) != NULL) return result;
#include
#include
char *safeget(char *buffer, int count) {
else if (count == 1) *result = '\0';
}
Khai thác lỗi overflow như thế nào ?
Bộ nhớ máy tính
Code khác
Chương trình đang chạy
Mảng có khả năng bị tràn
Ghi vào mảng (gửi dữ liệu có chứa chương trình gây hại vào CT đang chạy )
Lots of "no operations"
Chương trình gây hại
Chương trình đang chạy thử tiếp tục chạy trên phần bộ nhớ dành cho nó, nhưng mà phần bộ nhớ này đã bị chương trình gây hại chiếm mất khi thực hiện phép ghi vào mảng
II. LẬP TRÌNH PHÒNG NGỪA
1. Khái niệm
• Xuất phát từ khái niệm
tâm niệm rằng bạn không bao giờ biết chắc được người lái xe khác sẽ làm gì. Bằng cách đó, bạn có thể chắc chắn rằng khi họ làm điều gì nguy hiểm, thì bạn sẽ không bị ảnh hưởng (tai nạn).
– Bạn có trách nhiệm bảo vệ
bản thân, ngay cả khi người khác có lỗi
defensive driving. – Khi lái xe bạn luôn phải
1. Khái niệm
• Ý tưởng chính: nếu chương trình (CTC) nhận dữ liệu vào bị lỗi thì nó vẫn chạy thông, ngay cả khi CT khác cũng nhận dữ liệu đầu vào đó đã bị lỗi. • Lập trình phòng ngừa là cách tự bảo vệ CT của
mình khỏi – các ảnh hưởng tiêu cực của dữ liệu không hợp lệ – các rủi ro đến từ các sự kiện tưởng như "không bao giờ"
xảy ra
– sai lầm của các lập trình viên khác
2. Các lỗi có thể phòng ngừa
• Lỗi liên quan đến phần cứng
– Đảm bảo các lỗi như buffer overflows hay divide by zero
• Lỗi liên quan đến chương trình
được kiểm soát
– Đảm bảo giá trị gán cho các biến luôn nằm trong vùng
– Do not trust anything; verify everything
• Lỗi liên quan đến người dùng
kiểm soát
– Đừng cho rằng người dùng luôn thực hiện đúng các thao
• Lỗi liên quan đến các kỹ thuật phòng ngừa!
tác theo chỉ dẫn, hãy kiểm tra mọi thao tác của họ
– Mã nguồn cài đặt các kỹ thuật phòng ngừa cũng có khả
năng gây lỗi, kiểm tra kỹ phần này
3. Các giai đoạn lập trình phòng ngừa
– Dành thời gian để kiểm tra và gỡ rối chương trình cẩn thận : hoàn
thành chương trình trước ít nhất 3 ngày so với hạn nộp
• Lập kế hoạch thực hiện công việc:
– Thiết kế giải thuật trước khi viết bằng ngôn ngữ lập trình cụ thể
• Thiết kế chương trình:
– Viết và kiểm thử từng phần chương trình: phần chương trình nào
dùng để làm gì
– Viết và kiểm thử mối liên kết giữa các phần trong chương trình:
quy trình nghiệp vụ như thế nào
– Phòng ngừa bằng các điều kiện trước và sau khi gọi mỗi phần
chương trình: điều gì phải đúng trước khi gọi chương trình, điều gì xảy ra sau khi chương trình thực hiện xong
– Dùng chú thích để miêu tả cấu trúc chương trình khi viết chương
trình
• Giữ vững cấu trúc chương trình:
Kiểm tra cái gì, khi nào ?
• Testing: chỉ ra các vấn đề làm chương trình không
chạy
• Kiểm tra theo cấu trúc của chương trình: Kiểm tra việc thực hiện các nhiệm vụ đặt ra cho từng phần chương trình – Ví dụ: điều gì xảy ra với chương trình căn lề văn bản, nếu
hàm ReadWord() bị lỗi ?
• Nếu chương trình không có tham số đầu vào, mà chỉ thực thi nhiệm vụ và sinh ra kết quả thì không cần kiểm tra nhiều. Hầu hết chương trình đều không như vậy – Ví dụ: điều gì xảy ra với chương trình căn lề văn bản, nếu
• Không nhập đầu vào ? • Đầu vào không phải là xâu/file chứa các từ hay chữ cái đúng
quy định ?
a. Kiểm tra tham số đầu vào
• Một phần chương trình chạy thông 1 lần
không có nghĩa là lần tiếp theo nó sẽ chạy thông.
• Chương trình trả ra kết quả đúng với đầu vào 'n' không có nghĩa là nó sẽ trả ra kết quả đúng với đầu vào ‘m’ <> ‘n’.
• Vậy chương trình có thực sự chạy thông
không ? – Với bất cứ đầu vào nào chương trình cũng
phải chạy thông, không bị “crash”. Nếu có lỗi thì chương trình phải dừng và thông báo lỗi – Bạn có thể biết chương trình có chạy thông
hay không khi kiểm tra chương trình bằng các tham số đầu vào sai
Tham số đầu vào sai
• Trong thực tiễn: “Garbage in,
garbage out.” – GIGO
• Trong lập trình, “rác vào – rác ra” là dấu hiệu của những CT tồi, không an toàn
• Với 1 CT tốt thì:
– “rác vào, không có gì ra”,
– “rác vào, có thông báo lỗi”
– “không cho phép rác vào”.
Phòng ngừa lỗi tham số vào
• Check the values of all routine input parameters – Kiểm tra giá trị của tất cả các tham số truyền vào các
• Decide how to handle bad inputs
hàm cùng cần như kiểm tra dữ liệu nhập từ nguồn ngoài khác
– Khi phát hiện 1 tham số hay 1 dữ liệu không hợp lệ, bạn
cần làm gì với nó? Tùy thuộc tình huống, bạn có thể chọn 1 trong các phương án phù hợp
Phòng ngừa lỗi tham số vào
• Kiểm tra giá trị của mọi dữ liệu từ nguồn bên
ngoài – Khi nhận dữ liệu từ file, bàn phím, mạng, hoặc từ các
nguồn ngoài khác, hãy kiểm tra để đảm bảo rằng dữ liệu nằm trong giới hạn cho phép.
phải đủ ngẵn để xử lý
• Nếu một chuỗi cần trong một phạm vi giới hạn của các giá trị (như một ID giao dịch tài chính…), hãy chắc chắn rằng chuỗi đầu vào là hợp lệ cho mục đích của nó; nếu không từ chối.
– Hãy đảm bảo rằng giá trị số nằm trong dung sai và xâu
– Với ứng dụng bảo mật, hãy đặc biệt lưu ý đến những dữ
liệu có thể tấn công hệ thống: Cố làm tràn bộ nhớ, injected SQL commands, injected html hay XML code, tràn số …
Ví dụ
• Đoạn mã nguồn sau tìm giá trị trung bình của n
giá trị kiểu doubles.
• Chương trình bị lỗi khi nào ?
double avg (double a[], int n) /* a là mảng gồm n số kiểu doubles */ { int i; double sum= 0; for (i= 0; i < n; i++) { sum+= a[i]; } return sum/n; }
Phòng ngừa lỗi tham số vào
• Trong một số trường hợp, phải viết thêm các
đoạn mã nguồn để lọc giá trị đầu vào trước khi tính toán
void class_of_degree (char degree[], double percent) /* Xếp hạng sinh viên dựa vào tổng điểm tính theo % */ { if (percent < 0 || percent > 100) strcpy(degree,"Error in mark"); else if (percent >= 70) strcpy(degree,"First"); else if (percent >= 60) strcpy(degree,"Two-one"); . . }
Kiểm tra điều kiện biên
• Điều gì xảy ra nếu giá trị đầu vào quá
lớn hay quá nhỏ ?
• Hãy chắc chắn là chương trình của bạn có thể đối phó với các tham số đầu vào kiểu này
• Luôn kiểm tra trường hợp “divide by
zero error”
Ví dụ
• Hàm sau đây mô phỏng hàm strlen trong thư
viện chuẩn của C. Viết thêm các dòng lệnh phòng ngừa lỗi int my_strlen (char *string) /* Khi tính độ dài xâu, hàm này sai ở đâu */ { int len= 1; while (string[len] != '\0') len++; return len; }
c. Tràn số (overflow of numbers)
• • • •
•
Arian 5: Chi phí phát triển: 7 tỷ USD Phụ kiện hàng hóa đi kèm : 370 triệu USD Thực hiện chuyển đổi 64 bit dấu phẩy động sang 16 bit số nguyên: Việc chuyển đổi không thành công do tràn số 04/06/1996: 37 giây sau khi phóng, nổ ở độ cao 3700m
Tràn số (overflows of numbers)
• Nếu cần tính toán với các số lớn, hãy chắc chắn là bạn biết giá trị lớn nhất mà biến bạn dùng có khả năng lưu trữ
• Ví dụ:
đến 255.
– Với phần lớn trình dịch C, 1 unsigned char có giá trị từ 0
– Kích thước tối đa của 1 biến kiểu int có thể thay đổi
5. Kiểm soát lỗi có thể xảy ra
• Error handling: xử lý các lỗi mà ta dự kiến sẽ xảy
ra
• Tùy theo tình huống cụ thể, ta có thể trả về:
– 1 giá trị trung lập – thay thế đoạn tiếp theo của dữ liệu hợp lệ – trả về cùng giá trị như lần trước – thay thế giá trị hợp lệ gần nhất – ghi vết 1 cảnh báo vào tệp – trả về 1 mã lỗi – gọi 1 thủ tục hay đối tượng xử lý – hiện 1 thông báo hay tắt máy
Chắc chắn hay chính xác ?
• Chắc chắn: CT luôn chạy thông, kể cả khi có lỗi • Chính xác: CT không bao giờ gặp lại lỗi • Ví dụ: Lỗi hiện thị trong các trình xử lý văn bản:
khi đang thay đổi nội dung văn bản, thỉnh thoảng một phần của một dòng văn bản ở phía dưới màn hình bị hiện thị sai. Khi đó người dùng phải làm gì? – Tắt CT – Nhấn PgUp hoặc PgDn, màn hình sẽ làm mới
• Ưu tiên tính chắc chắn thay vì tính chính xác:
– Bất cứ kết quả nào đó bao giờ cũng thường là tốt hơn so
với Shutdown.
Khi nào phải loại bỏ hết lỗi ?
• Đôi khi, để loại bỏ 1 lỗi nhỏ, lại rất tốn kém
– Nếu lỗi đó chắc chắn không ảnh hưởng đến mục đích cơ bản của ứng dụng, không làm chương trình bị treo, hoặc làm sai lệch kết quả chính, người ta có thể bỏ qua, mà không cố sửa để có thể gặp phải các nguy cơ khác. • Phần mềm “chịu lỗi”?: Phần mềm sống chung với
lỗi, để đảm bảo tính liên tục, ổn định
5. Xử lý ngoại lệ
thống (tràn bộ nhớ).
• Một chương trình biên dịch thành công, vẫn có thể gây ra những lỗi khi chạy, đó chính là các ngoại lệ (exception). – Điều kiện chủ quan: Do lập trình sai – Điều kiện khách quan: Do nhập sai dữ liệu, do trạng thái của hệ
• Ngoại lệ phá vỡ luồng bình thường của CT • Khi xảy ra một ngoại lệ, nếu không xử lý thì chương trình
NO HANDLER EXISTS
kết thúc ngay và trả lại quyền điều khiển cho hệ điều hành.
float number1, number2; //nhập vào 2 số float division=number1/number2;
EXIT
Báo hiệu tình tình huống bất thường
• Bắt các tình huống bất thường và phục hồi chúng về
trạng thái trước đó
• Giúp chương trình đáng tin cậy hơn, tránh kết thúc
bất thường
• Tách biệt khối lệnh có thể gây ngoại lệ và khối lệnh xử
lý ngoại lệ
Khối xử lý lỗi
………… IF B IS ZERO GO TO ERROR C = A/B PRINT C GO TO EXIT ERROR: DISPLAY “DIVISION BY ZERO” EXIT: END
Báo hiệu điều kiện lỗi
• Thông báo cho các bộ phận khác của chương
trình về lỗi không nên bỏ qua
• Chỉ dùng ngoại lệ cho những điều kiện thực sự
ngoại lệ – Exception được dùng trong những tình huống giống
assertion cho các sự kiện không thường xuyên, nhưng có thể không bao giờ xảy ra
– Exception có thể bị lạm dụng và phá vỡ các cấu trúc,
điều này dễ gây ra lỗi, vì làm sai lệch luồng điều khiển
Ví dụ chia cho 0
public class ChiaCho0Demo { public static void main(String args[]){ try { int num = calculate(9,0); System.out.println(num); } catch(Exception e) { System.err.println("Co loi xay ra: " + e.toString()); } } static int calculate(int no, int no1){ int num = no / no1; return num; } }
Phục hồi tài nguyên khi có ngoại lệ
• Thường thì không phục hồi tài nguyên • Nhưng sẽ hữu ích khi thực hiện các công việc
nhằm đảm bảo cho thông tin ở trạng thái rõ ràng và vô hại nhất có thể
• Nếu các biến vẫn còn được truy xuất thì chúng
nên được gán các giá trị hợp lý
• Trường hợp thực thi việc cập nhật dữ liệu, nhất là trong 1 phiên – transaction – liên quan tới nhiều bảng chính, phụ, thì việc khôi phục khi có ngoại lệ là vô cùng cần thiết (rollback)
Kết luận
Keeping It Simple (KISS): complex code is more likely to contain errors.
Controlling complexity is the essence of computer programming. -- Brian Wilson Kernighan
Kiểm tra mã nguồn trong khi viết chương trình
• 1 LTV giỏi không ngồi viết 1 lúc 10000 dòng lệnh rồi
chạy thử chương trình
• Mọi việc sẽ dễ dàng hơn khi vừa viết chương trình vừa
kiểm tra
• Xác định cấu trúc của chương trình, viết từng phần
nhỏ của chương trình và kiểm tra luôn phần vừa viết.
• Hoàn thiện từng phần chương trình, kiểm tra lại sau
khi hoàn thiện.
• Mẹo:
– Nếu có sửa đổi mã nguồn thì dịch từng đoạn một, khoảng
chục dòng
– Sử dụng nhiều cửa sổ dịch
III. KIỂM THỬ
a. Mục đích
• Khó có thể khẳng định 1 CT lớn có làm
việc chuẩn hay không
• Khi XD 1 CT lớn, 1 LTV chuyên nghiệp sẽ dành thời gian cho việc viết test code không ít hơn thời gian dành cho viết CT
• LTV chuyên nghiệp là người có khả
năng, kiến thức rộng về các kỹ thuật và chiến lược testing
b. Khái niệm
• Beizer: Testing = chứng minh tính
đúng đắn giữa 1 phần tử và các đặc tả của nó.
• Myers: Testing = quá trình thực hiện 1
CT với mục đích tìm ra những lỗi.
• IEEE: Testing = quá trình kiểm tra hay đánh giá 1 hệ thống hay 1 thành phần hệ thống một cách thủ công hay tự động để kiểm thử rằng nó thỏa mãn những yêu cầu đặc thù hoặc để xác định sự khác biệt giữa kết quả mong đợi và kết quả thực tế
c. Testing vs. debugging
• Testing & debugging đi cùng với
nhau như 1 cặp: – Testing tìm errors; debugging định vị
và sửa chúng.
– Ta có mô hình “testing/debugging
cycle”: Ta test, rồi debug, rồi lặp lại. – Bất kỳ 1 debugging nào nên được tiếp theo là 1 sự áp dụng lại của hàng loạt các tests liên quan, đặc biệt là các bài tests hồi quy. Điều này giúp tránh nảy sinh các lỗi mới khi debugging.
– Testing & debugging không nên được
thực hiện bởi cùng 1 người.
d. Testing vs. Program Verification
• Program
Specification
Right/Wrong
Program Checker
program.c
verification – Lý tưởng: Chứng
?
minh được rằng CT của ta là chính xác, đúng đắn
• Có thể chứng
minh các thuộc tính của CT? • Có thể CM điều đó kể cả khi CT kết thúc?!!!
Specification
• Testing:
Probably Right/Wrong
Testing Strategy
program.c
– Hiện thực : Thuyết phục bản thân rằng CT có thể làm việc
e. Nguyên tắc
• Ưu tiên việc kiểm thử, để khi kết thúc quá trình kiểm thử, luôn bảo đảm là việc kiểm thử đã được thực hiện tốt nhất có thể trong khoảng thời gian bạn có.
f. Các loại kiểm thử
• External testing
– Thiết kế dữ liệu để kiểm thử chương trình
• Internal testing
– Thiết kế chương trình để nó có thể kiểm
thử chính nó
Khái niệm
• External testing: Thiết kế dữ liệu để
kiểm thử chương trình
• Phân loại :
– Kiểm thử giá trị biên : Boundary testing – Kiểm thử lệnh : Statement testing – Kiểm thử luồng : Path testing – Kiểm thử sốc : Stress testing
a. Boundary Testing
• Hầu hết các lỗi đều xảy ra ở các điều kiện biên -
boundary conditions
• Nếu CT làm việc tốt ở đk biên, nó có thể sẽ làm
việc đúng với các đk khác
Boundary Testing là kỹ thuật kiểm thử sử dụng các giá trị nhập vào ở trên hoặc dưới một miền giới hạn của 1 đầu vào và với các giá trị đầu vào tạo ra các đầu ra ở biên của 1 đầu ra.
Ví dụ
• VD : đọc 1 dòng từ stdin và đưa vào mảng ký tự
int i; char s[MAXLINE]; for (i=0; (s[i]=getchar()) != '\n' && i < MAXLINE-1; i++) ; s[i] = '\0'; printf("String: |%s|\n", s);
• Xét các điều kiện biên
– Dòng rỗng –bắt đầu với '\n'
• In ra xâu rỗng (“\0”) => in ra “||” , ok
– Nếu gặp EOF trước '\n‘
• Tiếp tục gọi getchar() và lưu ӱ vào s[i], not ok
– Nếu gặp ngay EOF (empty file)
• Tiếp tục gọi getchar() và lưu ӱ vào s[i], not ok
Ví dụ
int i; char s[MAXLINE]; for (i=0; (s[i]=getchar()) != '\n' && i < MAXLINE-1; i++) ; s[i] = '\0'; printf("String: |%s|\n", s);
• Tiếp tục xét các ĐK biên (tt)
• In ra đúng, với ‘\0’ tại s[MAXLINE-1]
– Dòng chứa đúng MAXLINE-1 ký tự
• Ký tự cuối cùng bị ghi đè, và dòng mới không bao giờ đc
đọc
– Dòng dài hơn MAXLINE ký tự
• 1 số ký tự, kể cả newline, không đc đọc và sót lại trong
stdin
– Dòng chứa đúng MAXLINE ký tự
Boundary Testing Example (cont.)
• Rewrite the code
int i;
char s[MAXLINE];
for (i=0; i
• Trường hợp gặp EOF
for (i=0; i
• Các trường hợp khác?
This is wrong.
Why?
– Nearly full
– Exactly full
– Over full
Ví dụ
• Rewrite yet again
s[i] = '\0';
for (i=0; ; i++) {
int c = getchar();
if (c==EOF || c=='\n' || i==MAXLINE-1) {
break;
}
else s[i] = c;
}
There’s still a problem...
Where’s
the ‘d’?
Input:
Four
score and seven
years
Output:
FourØ
score anØ
sevenØ
yearsØ
Ambiguity in Specification
• Nếu dòng quá dài, xử lý thế nào?
– Giữ MAXLINE ký tự đầu, bỏ qua phần còn lại?
– Giữ MAXLINE ký tự đầu + ‘\0’, bỏ qua phần còn lại?
– Giữ MAXLINE ký tự đầu+’\0’, lưu phần còn lại cho lần gọi
sau của input function?
• Có thể phần đặc tả - specification không hề đề cập khi
MAXLINE bị quá
– Có thể người ta không muốn dòng dài quá giới hạn trong
mọi trường hợp
– Đạo đức: kiểm tra đã phát hiện ra một vấn đề thiết kế,
thậm chí có thể là một vấn đề đặc điểm kỹ thuật !
• Quyết định phải làm gì
– Cắt những dòng quá dài?
– Lưu phần còn lại để đọc như 1 dòng mới?
Kiểm tra điều kiện trước và sau
• Xác định những thuộc tính cần đi trước (điều kiện trước) và
sau (điều kiện sau) để mã nguồn được thi hành
• Ví dụ: các giá trị đầu vào phải thuộc 1 phạm vi cho trước
double avg( double a[], int n) {
int i; double sum=0.0;
for ( i = 0; i
sum+=a[i];
return sum/n;
• Nếu n=0 ?, nếu n<0 ?
}
• Có thể thay : return n <=0 ? 0.0: sum/n;
b. Statement Testing
• “Testing để thỏa mãn điều kiện rằng
mỗi lệnh trong 1 CT phải thực hiện ít
nhất 1 lần khi testing.”
Ví dụ
• Statement testing pseudocode:
Statement testing:
Phải chắc chắn các lệnh “if”
và 4 lệnh trong các nhánh
phải được thực hiện
if (condition1)
statement1;
else
statement2;
if (condition2)
statement3;
else
statement4;
…
– condition1 là đúng và condition2 là đúng
• Thực hiện statement1 và statement3
– condition1 là sai và condition2 là sai
• Thực hiện statement2 và statement4
• Đòi hỏi 2 tập dữ liệu. Ví dụ:
c. Path Testing
• Kiểm tra để đáp ứng các tiêu chuẩn đảm bảo
rằng mỗi đường dẫn logic xuyên suốt chương
trình được kiểm tra. Thường thì đường dẫn xuyên
suốt chương trình này được nhóm thành một tập
hữu hạn các lớp. Một đường dẫn từ mỗi lớp sau
đó được kiểm tra.
• Khó hơn nhiều so với statement testing
– Với các CT đơn giản, có thể liệt kê các nhánh đường dẫn
xuyên suốt code
– Ngược lại, bằng các đầu vào ngẫu nhiên tạo các đường
dẫn theo CT
Ví dụ
Path testing:
Cần đảm bảo tất cả các luồng
công việc đều được kiểm thử
if (condition1)
statement1;
else
statement2;
…
if (condition2)
statement3;
else
statement4;
…
• Đòi hỏi 4 tập dữ liệu:
– condition1 là true và condition2 là true
– condition1 là true và condition2 là false
– condition1 là false và condition2 là true
– condition1 là false và condition2 la false
• Chương trình thực tế => bùng nổ các tổ hợp!!!
Bài tập
1
F
else
A>0
T
3
2
B>0
T
F
(1) input(A,B)
if (A>0)
(2) Z = A;
(3) Z = 0;
if (B>0)
(4) Z = Z+B;
(5) output(Z)
4
5
What is the path condition for path <1,2,5>?
(A>0) Л (B0)
Bài tập
1
A>B
T
F
2
F
B<0
T
4
3
(1) input(A,B)
if (A>B)
(2) B = B*B;
if (B<0)
(3) Z = A;
else
(4) Z = B;
(5) output(Z)
5
What is the path condition for path <1,2,3,5>?
(A>B) Л (B<0)
Consider ANOTHER example…
1
A>B
T
F
F
2
T
B<0
T
4
3
5
What is the path condition for path <1,2,3,5>?
(A>B) Л (B<0) (B2<0) = FALSE
(1) input(A,B)
if (A>B)
(2) B = B*B;
if (B<0)
(3) Z = A;
else
(4) Z = B;
(5) output(Z)
d. Stress Testing
• “Tiến hành thử nghiệm để đánh giá một hệ thống
hay thành phần tại hoặc vượt quá các giới hạn
của các yêu cầu cụ thể của nó”
• Phải tạo:
– Một tập lớn đầu vào - Very large inputs
– Các đầu vào ngẫu nhiên - Random inputs (binary vs.
• Nên dùng máy tính để tạo đầu vào
ASCII)
Ví dụ: Stress Testing
• Example program:
Stress testing: Phải cung cấp
các đầu vào ngầu nhiên dạng
nhị phân hay ASCII
#include
int main(void) {
char c;
while ((c = getchar()) != EOF)
putchar(c);
return 0;
}
• Mục tiêu: Copy tất cả các ký tự từ stdin vào stdout; nhưng
lưu ý bug!!!
• Làm việc với tập dữ liệu ASCII chuẩn ( tự tạo)
• Máy tính tự tạo ngẫu nhiên tập dữ liệu dạng 255 (decimal),
hay 11111111 (binary), và EOF để dừng vòng lặp
Ví dụ: Stress Testing
Stress testing: Phải cung cấp
very large inputs
• Example program:
#include
int main(void) {
short charCount = 0;
while (getchar() != EOF)
charCount++;
printf("%hd\n", charCount);
return 0;
}
• Mục tiêu: Đếm và in số các kỹ tự trong stdin
• Làm việc với tập dữ liệu có kích thước phù hợp
• Sẽ có lỗi với tập dữ liệu do máy tạo chứa hơn
32767 characters
3. Internal Testing
• Internal testing: Thiết kế chương trình để nó tự
kiểm thử chính nó
• Internal testing techniques
conservation properties
(1) Kiểm tra các giá trị bất biến - Testing invariants
(2) Kiểm tra các thuộc tính cần được bảo lưu - Verifying
(3) Kiểm tra các giá trị trả về - Checking function return
values
(4) Thay đổi mã nguồn tạm thời - Changing code
temporarily
(5) Giữ lại phần mã kiểm tra - Leaving testing code intact
a. Testing Invariants
• Kiểm tra các điều kiện tiên nghiệm và điều kiện
hậu nghiệm
• 1 số khía cạnh của CTDL là bất biến, vì vậy, khi 1
hàm tác động lên CTDL, phải kiểm tra tính bất
biến của CTDL trước và trong khi tác động.
• Ví dụ: hàm thêm vào danh sách nối kép
– Duyệt danh sách nối kép
– Nếu node x trỏ đến node y, vậy node y có nhất định phải
• Ví dụ: hàm thêm nút vào cây nhị phân tìm kiếm
– Duyệt cây
– Sau khi thêm các nút còn được sắp đúng thứ tự không ?
trỏ đến node x hay không ?
b. Kiểm tra các đặc tính cần được bảo
lưu
• Khái quát hóa của testing invariants
• 1 hàm cần kiểm tra các cấu trúc dữ liệu bị tác
động tại các điểm đầu và cuối
• VD: hàm Str_concat()
• VD: Hàm chèn thêm PT vào danh sách -List
insertion function
– Tại điểm khởi đầu, tính độ dài ds
– Tại điểm cuối, Tính độ dài mới
– Độ dài mới = độ dài cũ + 1?
– Tại điểm đầu, tìm độ dài của 2 xâu đã cho; tính tổng
– Tại điểm cuối, tìm độ dài của xâu kết quả
– 2 độ dài có bằng nhau không ?
c. Kiểm tra các giá trị trả về
• Java/C++:
– Các phương thức phát hiện lỗi có thể bỏ qua 1 trường
hợp ngoại lệ đã được biết đến (“throw a checked
exception”)
– Việc gọi 1 phương thức cần phải kiểm soát được ngoại lệ
• C:
(hoặc lại bỏ qua ngoại lệ đó)
– Không có kỹ thuật kiểm soát các trường hợp ngoại lệ
– Hàm phát hiện ra lỗi thông qua giá trị trả về
– LTV thường quên kiểm tra giá trị trả về nên kiểm tra
giá trị trả về
Kiểm tra các giá trị trả về
Bad code
Good code
– scanf() trả về số giá trị đã đọc
int i;
scanf("%d", &i);
int i;
if (scanf("%d", &i) != 1)
/* Error */
– printf() có thể bị lỗi nếu ghi vào file hay đĩa đầy; trả
về số các ký tự (ko phải giá trị) đã ghi
Bad code??? Good code, or overkill???
int i = 100;
printf("%d", i);
int i = 100;
if (printf("%d", i) != 3)
/* Error */
d. Thay đổi mã nguồn tạm thời
• Thay đổi mã nguồn tạm thời để kiểm tra các điều kiện
biên và kiểm tra trực tiếp các giá trị
• Ví dụ: chương trình sắp xếp trên mảng:
– Dùng mảng với kích thước nhỏ
– CT có kiểm soát được lỗi tràn bộ nhớ hay không ?
• Nhớ áp dụng vào bài tập
• Ví dụ: dùng bảng băm
– Tạm thời cho hàm băm trả về kết quả là 1 hằng số
– Như vậy toàn bộ các giá trị cần băm sẽ có cùng 1 chỉ số,
tức là số lượng các giá trị cần băm có cùng chỉ số là rất lớn
– CT có kiểm soát được tình huống này hay không ?
Thay đổi mã nguồn tạm thời
• Viết 1 phiên bản hàm cấp phát bộ nhớ và phát
hiện ra lỗi sớm, để kiểm thử đoạn mã nguồn bị lỗi
thiếu bộ nhớ:
void *testmalloc( size_t n) {
static int count =0;
if (++count > 10)
return NULL;
else
return malloc(n);
}
e. Giữ lại phần mã kiểm tra
• Để nguyên phần mã kiểm tra quan trọng trong mã nguồn
• Có thể khoanh vùng chúng lại như là mã gỡ rối:
#ifndef NDEBUG … #endif
– Kích hoạt/ tắt các assert macro
– Kích hoạt/ tắt các đoạn mã gỡ rối của LTV
• Khi dịch thì dùng tùy chọn –DNDEBUG của gcc
– Việc viết các đoạn mã tự kiểm tra cho CT có thể làm giảm chi phí
bảo trì chương trình
– Mã nguồn sáng sủa cũng làm giảm chi phí bảo trì chương trình
– Nhưng: việc viết các đoạn mã tự kiểm tra khắp nơi sẽ làm
• Đề phòng xung đột:
giảm tính rõ ràng của chương trình
4. Các chiến lược kiểm thử tổng quát
• Kiểm thử dần dần -Testing incrementally
• So sánh các cách cài đặt khác nhau -Comparing
implementations
• Tự động kiểm thử – Automation
• Kiểm thử theo lỗi - Bug-driven testing
• Tiêm, gài lỗi - Fault injection
a. Testing Incrementally
• Test khi viết code
– Thêm tests khi tạo 1 lựa chọn mới - new cases
– Test phần đơn giản trước phần phức tạp
– Test units (tức là từng module riêng lẻ) trước khi testing
• Thực hiện regression testing – kiểm thử hồi quy
– Xử lý đc 1 lỗi thường tạo ra những lỗi mới trong 1 he
toàn hệ thống
thống lớn, vì vậy …
– Phải đảm bảo chắc chắn hệ thống không “thoái lui” kiểu
như chức năng trước kia đang làm việc giờ bị broken,
nên…
– Test mọi khả năng để so sanh phiên bản mới với phiên
bản cũ
Testing Incrementally (cont.)
• Tạo “giàn giáo” - scaffolds và “mẫu” -stubs để
test đoạn code mà ta quan tâm
Hàm gọi đến code
mà ta quan tâm
Đoạn code cần quan tâm
Hàm được gọi
bởi đoạn code cần
quan tâm
Hàm được gọi
bởi đoạn code cần
quan tâm
b. So sánh các cài đặt
• Hãy chắc chắn rằng các triển khai độc lập hành
xử như nhau
• Ví dụ: So sánh hành vi của chương trình mà bạn
dịch ( TB C++3.0 ) với GCC
• Ví dụ: So sánh hành vi của các hàm bạn tạo
trong str.h với các hàm trong thư viện string.h
• Trường hợp có thể tính 1 kết quả bẳng 2 cách
khác nhau (2 thuật toán giải quyết cùng 1 bài
toán): xây dựng 2 chương trình và kiểm tra kết
quả thực hiện
– Kết quả giống nhau: 2 chương trình cùng đúng
– Khác nhau: ít nhất 1 trong 2 chương trình bị sai
c. Tự động kiểm thử
• Kiểm thử thủ công rất nặng nhọc, tốn kém và nhàm
chán thậm chí không đủ độ tin cậy.
• Trong quá trình kiểm thử, phải
– Thực hiện lặp đi lặp lại các thao tác kiểm thử
– Dùng nhiều bộ dữ liệu nhập
– Nhiều lần so sánh dữ liệu đầu ra
• Tạo testing code để tránh các nhược điểm nói trên
– Viết 1 bộ kiểm thử để kiểm tra toàn bộ chương trình mỗi
khi có sự thay đổi, sau khi biên dịch thành công
– Cần biết cái gì được chờ đợi
• Tạo ra các đầu ra, sao cho dễ dàng nhận biết là đúng hay sai
– Tự động kiểm thử tốt hơn nhiều so với kiểm thử thủ công
Tự động hóa kiểm thử hồi quy
• Tuần tự so sánh kết quả thực hiện các phiên bản mới
với kết quả thực hiện những phiên bản cũ tương ứng.
• Mục đích : đảm bảo việc sửa lỗi sẽ không làm ảnh
hưởng những phần khác trừ khi chúng ta muốn
• 1 số hệ thống có công cụ trợ giúp kiểm thử tự động :
– Ngôn ngữ scripts : cho phép viết các đoạn script để test
tuần tự
– Unix : có các thao tác trên tệp tin như cmp và diff để so
sanh dữ liệu xuất, sort sắp xếp các phần tử, grep để kiểm
thử dữ liệu xuất, wc, sum va freq để tổng kết dữ liệu xuất
• Khi kiểm thử hồi quy, cần đảm bảo phiên bản cũ là
đúng, nếu sai thì rất khó xác định và kết quả sẽ
không chính xác
• Cần phải kiểm tra chính việc kiểm thử hồi quy 1 cách
định kỳ để đảm bảo nó vẫn hợp lệ
Tạo ra những kiểm thử độc lập
• Kiểm thử độc lập với các giá trị nhập và giá trị xuất mong đợi sẽ bổ
xung cho kiểm thử hồi quy
• VD: Dùng ngôn ngữ NewAwk thực hiện kiểm thử 1 CT ngắn, ghi dữ
liệu đẩu ra của CT vào 1 tệp, ghi kết quả đúng vào 1 tệp khác rồi so
sánh 2 tệp, nếu khác nhau thì thông báo lỗi
echo 3 5 | newawk ‘{i=1; print ($si)++; print $i ,i}’ > out1
echo ‘3 4 1’ > out2 #kết quả đúng
if ! Cmp –s out1 out2 # nếu kq so sánh khác nhau
then
echo ‘BAD: test failed’
Fi
• Mọt lỗi có thể cần nhiều thao tác kiểm thử hoặc phải kiểm tra toàn bộ
các lớp mới, hoặc có thể thêm vào những đoạn CT bảo vệ để có thể
bắt đc những lỗi trong CT
d. Bug-Driven Testing
• Tìm thấy 1 bug
Ngay lập tức tạo 1 test để bắt lỗi
• Đơn giản hóa việc kiểm thử hồi quy
e. Fault injection
• Chủ động (tạm thời) cài các bugs!!!
• Rồi quyết định nếu tìm thấy chúng
• Kiểm thử chính đoạn mã kiểm thử/ quy trình
kiểm thử
4. Ai kiểm thử cái gì
• Programmers: White-box testing
– Thuận lợi: Người triển khai nắm rõ mọi luồng dữ liệu
– Khó khăn: Bị ảnh hưởng bởi cách thiết kế CT hay viết
• Quality Assurance (QA) engineers: Black-box
testing
– Thuận lợi: Không có khái niệm về implementation
– Khó khăn: Không muốn test mọi logical paths
• Customers: Field testing
– Thuận lợi: Có các cách sử dụng CT bất ngờ; dễ gây lỗi
– Khó khăn: Không đủ trường hợp; khách hàng không
mã nguồn
thích tham gia vào quá trình test
Các kỹ thuật kiểm thử
• Black-Box: Testing chỉ dựa trên việc
phân tích các yêu cầu - requirements
(unit/component specification, user
documentation, v.v.). Còn được gọi là
functional testing.
• White-Box: Testing dựa trên việc
phân tích các logic bên trong - internal
logic (design, code, v.v.). (Nhưng kết
quả mong đợi vẫn đến từ
requirements.) Còn đc gọi là structural
testing.
Các mức độ kiểm thử
• Unit: testing các mẫu công việc nhỏ nhất của LTV để có thể
lập kế hoạch và theo dõi hợp lý (vd : function, procedure,
module, object class, ….)
• Component: testing 1 tập hợp các units tạo thành 1 thành
phần (vd : program, package, task, interacting object
classes, …)
• Product: testing các thành phần tạo thành 1 sản phẩm (
subsystem, application, …)
– Bắt đầu = functional (black-box) tests,
– Rồi thêm = structural (white-box) tests, và
– Tiến hành từ unit level đến system level với 1 hoặc một vài bước
tích hợp
• System: testing toàn bộ hệ thống
• Testing thường:
Tại sao không "test everything"?
20 x 4 x 3 x 10 x 2 x 100 = 480,000 tests
Chi phí cho 'exhaustive' testing:
Nếu 1 giây cho 1 test, 8000 phút, 133 giờ, 17.7 ngày
(chưa kể nhầm lẫn hoặc test đi test lại)
nếu 10 secs = 34 wks, 1 min = 4 yrs, 10 min = 40 yrs
Kiểm thử bao nhiêu thì đủ?
• Không bao giờ đủ !
• Khi bạn thực hiện những test mà bạn đã lên kế hoạch
• Khi khách hàng/người sử dụng thấy thỏa mãn
• Khi bạn đã chứng minh đc rằng hệ thống hoạt động
đúng, chính xác
• Khi bạn tin tưởng rằng HT hoạt động tốt
• Phụ thuộc vào các rủi ro có thể xảy ra với hệ thống
– Thời gian để test luôn có giới hạn
– Dùng RISK để xác định:
• Cái gì phải test trước
• Cái gì phải test nhiều
• Mỗi phần tử cần test kỹ đến mức nào?
• Tại một thời điểm nhất định, cái gì không cần test
IV. GỠ RỐI
1. Tổng quan về gỡ rối chương trình
2. Tìm kiếm và gỡ rối
Mở đầu
• Gỡ rối chương trình lớn là công việc không đơn
giản
• 1 LTV giỏi phải biết
• Debuggers
• Version control systems
– Nhiều chiến lược gỡ rối khác nhau
– Các công cụ hỗ trợ việc gỡ rối
1. Tổng quan về gỡ rối chương trình
• Gỡ rối là gì ?
– Khi chương trình bị lỗi, gỡ rối là các công việc cần làm để làm cho chương trình dịch
thông, chạy thông
– Thật không may, gỡ rối luôn là thao tác phải làm khi lập trình, thao tác này rất tốn
kém
Cách tốt nhất vẫn là phòng ngừa
•
– Khi bắt đầu gỡ rối chương trình, bạn đã biết là chương trình không chạy.
– Nếu bạn biết lý do tại sao chương trình không chạy, bạn có thể sửa được chương trình
cho nó chạy
– Nếu bạn hiểu chương trình của bạn, bạn sẽ có ít sai lầm và dễ dàng sửa chữa sai sót
hơn. Bí quyết là viết mã đơn giản, hiệu quả, chú thích hợp lý.
• Đối với mã nguồn, tiêu chí nào quan trọng hơn: rõ ràng hay chính xác ?
– Nếu mã nguồn rõ ràng, bạn có thể làm cho chương trình trở nên chính xác.
– Bạn có chắc là làm cho chương trình trở nên chính xác nếu nó không rõ ràng hay
không ?
• Nếu chương trình được thiết kế với cấu trúc tốt, được viết bằng phong cách lập
trình tốt và áp dụng các kỹ thuật viết chương trình hiệu quả, bẫy lỗi thì chi phí
cho việc gỡ rối sẽ được giảm thiểu.
2. Tìm kiếm và gỡ rối
• Khi có lỗi, ta thường đổ cho trình dịch, thư viện hay bất cứ nguyên
nhân khách quan nào khác …tuy nhiên, cuối cùng thì lỗi vẫn là lỗi của
CT, và trách nhiệm gỡ rối thuộc về LTV
Phải hiểu vấn đề xuất phát từ đâu thì mới giải quyết được:
•
– Lỗi xảy ra ở đâu? Hầu hết các lỗi thường đơn giản và dễ tìm. Hãy khảo sát các
đầu mối và cố gắng xác định được đoạn mã nguồn gây lỗi
– Lỗi xảy ra như thế nào? Khi đã có 1 số thông tin về lỗi và nơi xảy ra lỗi, hãy
suy nghĩ xem lỗi xảy ra như thế nào
– Đâu là nguyên nhân gây lỗi? Suy luận ngược trở lại trạng thái của CT để xác
định nguyên nhân gây ra lỗi
• Gỡ rối liên quan đến việc suy luận lùi, giỗng như phá án.
1 số vấn đề không thể xảy ra và chỉ có những thông tin xác thực mới
đáng tin cậy
Phải đi ngược từ kết quả để khám phá nguyên nhân,
Khi có lời giải thích đầy đủ, ta sẽ biết được vấn đề cần sửa và có thể
phát hiện ra 1 số vấn đề khác
2. Tìm kiếm và gỡ rối
Debugging Heuristic Áp dụng khi nào
(2) Nghĩ trước khi viết lại chương trình
(1) Hiểu các thông báo lỗi (error messages) Build-time (dịch)
(3) Tìm kiếm các lỗi (bug) hay xảy ra
(4) Divide and conquer Run-time (chạy)
(5) Viết thêm các đoạn mã kiểm tra để
chương trình tự kiểm tra nó
(6) Hiện thị kết quả
(7) Sử dụng debugger
(8) Tập trung vào các lệnh mới viết / mới viết
lại
2.1. Hiểu các thông báo lỗi
Gỡ rối khi dịch (build-time) CT dễ hơn gỡ rối khi
chạy CT nếu LTV hiểu được các thông báo lỗi
Một số thông báo lỗi đến từ preprocessor
Gõ sai tên file cần gọi
Thiếu dấu */
#include
int main(void)
/* Print "hello, world" to stdout and
return 0.
{
printf("hello, world\n");
return 0;
}
$ gcc217 hello.c -o hello
hello.c:1:20: stdioo.h: No such file or directory
hello.c:3:1: unterminated comment
hello.c:2: error: syntax error at end of input
Hiểu các thông báo lỗi
• Một số thông báo lỗi đến từ compiler
#include
int main(void)
/* Print "hello, world" to stdout and
return 0. */
{
printf("hello, world\n")
retun 0;
}
Hiểu các thông báo lỗi
• Một số thông báo lỗi đến từ linker
#include
int main(void)
/* Print "hello, world" to stdout and
return 0. */
{
prinf("hello, world\n")
return 0;
}
2.2. Nghĩ trước khi viết lại chương
trình
Việc thay đổi mã nguồn không hợp lý có thể gây ra
nhiều vấn đề hơn là để nguyên không thay đổi gì,
nên…
Phải nghĩ trước khi làm
Nghĩ trước khi viết lại chương trình
• Gỡ rối ngay khi gặp
– Khi phát hiện lỗi, hãy sửa ngay, đừng để sau mới sửa, vì có thể lỗi
không xuất hiện lại (do tình huống)
– Cân nhắc: việc sửa chữa này có ảnh hưởng tới các tình huống khác
hay không ?
• Quan sát lỗi từ góc độ khác
– Viết đoạn mã nguồn gây lỗi ra giấy
• Đừng chép hết cả đoạn không có nguy cơ gây lỗi, hoặc in toàn bộ
code ra giấy in => phá vỡ cây cấu trúc
– Vẽ hình minh họa các cấu trúc dữ liệu
• Nếu mà giải thuật làm thay đổi CTDL, vẽ lại hình trước khi viết lại
giải thuật
– Đọc trước khi gõ vào
• Đừng vội vàng, khi không rõ điều gì thực sự gây ra lỗi và sửa
không đúng chỗ sẽ có nguy cơ gây ra lỗi khác
Nghĩ trước khi viết lại chương trình
• Tạm dừng viết CT
– Khi gặp vấn đề, khó khăn, chậm tiến độ, lập tức thay đổi công việc =>
rút ra khỏi luồng quán tính sai lầm …
– Bỏ qua đoạn CT có lỗi
– Khi nào cảm thấy sẵn sàng thì chữa
• Giải thích logic của đoạn mã nguồn:
– Cho chính bạn
• Tạo điều kiện để suy nghĩ lại
– Cho ai khác có thể phản bác
• Extrem programming : làm việc theo cặp, pair programming,
người này LT, người kia kiểm tra, và ngược lại
– Cho cái gì đó không thể phản bác (cây, cốc trà đá, gấu bông, cún,…)
• Tạo điều kiện củng cố suy luận của mình
2.3. Tìm kiếm các lỗi hay xảy ra
• Xem lại các quy tắc lập trình
int i;
…
scanf("%d", i);
char c;
…
c = getchar();
switch (i) {
case 0:
…
/* missing break */
case 1:
…
break;
…
}
if (i = 5)
…
while (c = getchar() != EOF)
…
if (5 < i < 10)
…
if (i & j)
…
Chú ý: nếu đặt chế độ
cảnh báo (warnings) khi
dịch thì hầu hết các lỗi
kiểu này sẽ được phát hiện
Tìm kiếm các lỗi hay xảy ra
• Khi gặp vấn đề, hãy liên tưởng đến những trường
hợp tương tự đã gặp
– Vd1 :
int n; scanf(“%d”,n); ?
– Vd2 :
int n=1; double d=PI;
printf(“%d %f \n”,d,n); ??
• Không khởi tạo biến (với C) cũng sẽ gây ra những
lỗi khó lường.
Tìm kiếm các lỗi hay xảy ra
• Làm cho lỗi xuất hiện lại
– Cố gắng làm cho lỗi có thể xuất hiện lại khi cần
– Nếu không được, thì thử tìm nguyên nhân tại sao lại
không làm cho lỗi xuất hiện lại
Các phương pháp gỡ rối
• Tránh mắc cùng 1 lỗi 2 lần : Sau khi sửa 1 lỗi, hãy suy nghĩ
xem có lỗi tương tự ở nơi nào khác không. VD :
break;
case ‘o’ : /* tên tệp ouput*/
outname = argv[i]; break;
case ‘f’ : from = atoi(argv[i]); break;
case ‘t’ : to = atoi(argv[i]); break;
for (i=1;i
2.4. Divide and Conquer
• Thu hẹp phạm vi
• Tập trung vào dữ liệu gây lỗi
Divide and Conquer
– Thử chương trình với các tham số đầu vào từ đơn giản đến phức
tạp, từ nhỏ đến lớn để tìm lỗi
– Ví dụ: chương trình lỗi với file đầu vào filex
• Tạo ra phiên bản copy của filex , tên là filexcopy
• Xoá bớt nửa sau của filexcopy
• Chạy chương trình với tham số đầu vào là filexcopy
– Nếu chạy thông => nửa đầu của filex không gây lỗi, loại bỏ nửa này,
tìm lỗi trong nửa sau của filex
– Nếu không chạy => nửa đầu của filex gây lỗi, loại bỏ nửa sau, tìm lỗi
trong nửa đầu của filex
• Lặp cho đến khi không còn dòng nào trong filex có thể bị loại bỏ
– Cách khác: bắt đầu với 1 ít dòng trong filex, thêm dần các dòng
vào cho đến khi thấy lỗi
• Khử đầu vào
Divide and Conquer
• Khử mã nguồn
– Thử chương trình với các đoạn mã nguồn từ ngắn đến
dài để tìm lỗi
– Example: đoạn chương trình có sử dụng đến các phần
• Viết riêng từng lời gọi hàm trong đoạn chương trình bị lỗi
(test client)
– Hoặc viết thêm phần kiểm tra gọi hàm vào phần CTC được gọi
• Chạy thử test client
• Không chạy => lỗi liên quan đến việc gọi/ thực hiện CTC
vừa thử
• Chạy thông => lỗi nằm trong phần còn lại, tiếp tục thử gọi
các hàm khác
CTC khác không chạy
2.5. Viết thêm các đoạn mã kiểm tra để
chương trình tự kiểm tra nó
• Dùng internal test để khử lỗi trong CT và giảm
nhẹ công việc tìm kiếm lỗi
– Chỉ cần viết thêm 1 hàm để kiểm tra, gắn vào trước và
sau đoạn có nguy cơ, comment lại sau khi đã xử lý xong
lỗi
đã học
• Kiểm tra các giá trị không thay đổi
• Kiểm tra các đặc tính cần bảo lưu
• Kiểm tra giá trị trả về của hàm
• Thay đổi mã nguồn tạm thời
• Kiểm tra mà không làm thay đổi mã nguồn
• Dùng assertion để nhận dạng các lỗi có trong CT
– Các kỹ thuật viết thêm mã tự kiểm tra cho chương trình
Assertions
• Assertion: 1 macro hay 1 CT con dùng trong quá trình
phát triển ứng dụng, cho phép CT tự kiểm tra khi
chạy.
• Return true >> OK, false >> có 1 lỗi gì đó trong CT.
• Ghi lại những giả thiết được đưa ra trong code
• Loại bỏ những điều kiện không mong đợi
• VD :
– Nếu hệ thống cho rằng file dữ liệu về khách hàng không
bao giờ vượt quá 50 000 bản ghi, CT có thể chứa 1
assertion rằng số bản ghi là <= 50 000.
– Khi mà số bản ghi <= 50,000, assertion sẽ không có phản
ứng gì.
– Nếu đếm đc hơn 50 000 bản ghi, nó sẽ lớn tiếng “khẳng
định” rằng có 1 lỗi trong CT
Assertions
• Assertions có thể được dùng để kiểm tra các giả thiết
như :
– Các tham số đầu vào nằm trong phạm vi mong đợi (tương
tự với các tham số đầu ra)
– File hay stream đang được mở (hay đóng) khi 1 CTC bắt
đầu thực hiện (hay kết thúc)
– 1 file hay stream đang ở bản ghi đầu tiên (hay cuối cùng)
khi 1 CTC bắt đầu ( hay kết thúc) thực hiện
– 1 file hay stream được mở để đọc, để ghi, hay cả đọc và ghi
– Giá trị của 1 tham số đầu vào là không thay đổi bởi 1 CTC
– 1 pointer là non-NULL
– 1 mảng đc truyền vào CTC có thể chứa ít nhất X phần tử
– 1 bảng đã đc khởi tạo để chứa các giá trị thực
– 1 danh sách là rỗng (hay đầy) lkhi 1 CTC bắt đầu (hay kết
thúc) thực hiện
Assertions
• End users không cần thấy
các thông báo của assertion ;
• Assertions chủ yếu đc dùng
trong quá trình phát triển
hay bảo dưỡng ứng dụng.
• Dịch thành code khi phát
triển, loại bỏ khỏi code trong
sản phẩm để nâng cao hiệu
năng của chương trình
• Rất nhiều NNLT hỗ trợ
assertions : C++, Java và
Visual Basic.
VD:
#define ASSERT(condition, message)
{
if ( !(condition) ) {
fprintf(
stderr,
"Assertion %s failed: %s\n",
condition,
message);
exit( EXIT_FAILURE );
}
}
• Kể cả khi NNLT không hỗ trợ,
thì cũng có thể dễ dàng xây
dựng
Dùng assertions như thế nào ?
• Bẫy lỗi cho những tình huống lường trước (sự
kiện ta chờ đợi sẽ xảy ra);
– Error-handling : checks for bad input data Hướng tới
• Dùng assertions cho các tình huống không lường
trước (sự kiện không mong đợi xảy ra hoặc không
bao giờ xảy ra)
– Assertions : check for bugs in the code hướng đến
việc xử lý lỗi
• Tránh đưa code xử lý vào trong assertions
– Điều gì xảy ra khi ta turn off the assertions ?
việc hiệu chỉnh chương trình, tạo ra phiên bản mới của
chương trình
Dùng assertions như thế nào ?
• Các chương trình lớn:
– trước tiên xác nhận lỗi (dùng assertion),
– sau đó bẫy lỗi (dùng error-handling)
• Nguyên nhân gây lỗi đã được xác định:
– hoặc dùng assertion, hoặc dùng error-handling,
– không dùng cả 2 cùng lúc
• Các chương trình cực lớn, nhiều người cùng phát triển
trong thời gian 5-10 năm, hoặc hơn nữa ?
– Cả assertions và error handling code có thể đc dùng cho
cùng 1 lỗi.
– Ví dụ trong source code cho Microsoft Word, những điều
kiện luôn trả về true thì đc dùng assertion, nhưng đồng thời
cũng đc xử lý.
– Assertions rất có lợi vì nó giúp loại bỏ rất nhiều lỗi trong
quá trình phát triển hệ thống
Kiểm tra các giá trị không thay đổi
• Có thể sử dụng assert để kiểm tra các giá trị không thay đổi
#ifndef NDEBUG
int isValid(MyType object) {
…
Test invariants here.
Return 1 (TRUE) if object passes
all tests, and 0 (FALSE) otherwise.
…
}
#endif
void myFunction(MyType object) {
assert(isValid(object));
…
Manipulate object here.
…
assert(isValid(object));
}
2.6. Hiện thị kết quả đầu ra
•
In giá trị của các biến tại các
điểm có khả năng gây lỗi để
định vị khu vực gây lỗi, hoặc
• Xác định tiến trình thực hiện :
“đến đây 1”
• Poor:
stdout is buffered;
CT có thể có lỗi
trước khi hiện ra
output
printf("%d", keyvariable);
• Maybe better:
printf("%d\n", keyvariable);
• Better:
In '\n' sẽ xóa bộ nhớ
đệm stdout , nhưng
sẽ không xóa khi in
ra file
Gọi fflush() để làm
sạch buffer 1 cách
tường minh
printf("%d", keyvariable);
fflush(stdout);
Hiện thị kết quả đầu ra
• Tạo log file
• Lưu vết
– Giúp ghi nhớ đc các vấn đề đã xảy ra,
và giải quyết các vđề tương tự sau
này, cũng như khi chuyển giao CT
cho người khác..
• Maybe even better:
In debugging
output ra stderr;
debugging output
có thể tách biệt
với đầu ra thông
thường bằng cách
in ấn của CT
fprintf(stderr, "%d", keyvariable);
• Maybe better still:
Ghi ra 1 a log file
Ngoài ra: stderr
không dùng buffer
FILE *fp = fopen("logfile", "w");
…
fprintf(fp, "%d", keyvariable);
fflush(fp);
2.7. Sử dụng trình gỡ rối (debugger)
IDE : kết hợp soạn thảo, biên dịch, gỡ rối …
•
• Các trình gỡ rối với giao diện đồ họa cho phép chạy chương
trình từng bước qua từng lệnh hoặc từng hàm, dừng ở những
dòng lệnh đặc biệt hay khi xuất hiện những đk đặc biệt, bên
canh đó có các công cụ cho phép định dạng và hiển thị giá trị
các biến, biểu thức
• Trình gỡ rối có thể được kích hoạt trực tiếp khi có lỗi hoặc gắn
vào chương trình đang chạy.
• Thường để tìm ra lỗi , ta phải xem xét thứ tự các hàm đã đc
kích hoạt ( theo vết) và hiển thị các giá trị các biến liên quan
• Nếu vẫn không phát hiện đc lỗi : dùng các BreakPoint hoạc
chạy từng bước – step by step
• Có nhiều công cụ gỡ rối mạnh và hiệu quả, tại sao ta vẫn mất
nhiều thời gian và trí lực để gỡ rối ?
• Nhiều khi các công cụ không thể giúp dễ dàng tìm lỗi, nếu đưa
ra 1 câu hỏi sai, trình gỡ rối sẽ cho 1 câu trả lời, nhưng ta có
thể không biết là nó đang bị sai
Sử dụng trình gỡ rối
• Dùng 1 trình gỡ rối để chạy từng bước là phương sách cuối cùng
• Nhiều khi vấn đề tưởng quá đơn giản nhưng lại không phát hiện được,
ví dụ các toán tử so sánh trong pascal va VB có độ ưu tiên ngang
nhau, nhưng với C ?
( == và != nhỏ hơn !)
Thứ tự các đối số của lời gọi hàm : ví dụ : strcpy(s1,s2)
•
•
int m[6]={1,2,3,4,5,6}, *p,*q;
p=m; q=p+2; *p++ =*q++; *p=*q; ???
Lỗi loại này khó tìm vì bản thân ý nghĩ của ta vạch ra 1 hướng suy
nghĩ sai lệch : coi điều không đúng là đúng
• Đôi khi lỗi là do nguyên nhân khách quan : Trình biên dịch, thư viện
hay hệ điều hành, hoặc lỗi phần cứng : 1994 lỗi xử lý dấu chấm
độngng trong bộ xử lý Pentium
Công cụ hỗ trợ gỡ rối
GDB: The GNU Project Debugger
• Gỡ rối được các chương trình viết bằng Ada, C,
C++, Objective-C, Pascal, v.v., chạy trên cùng
máy cài đặt GDB hay trên máy khác
• Hoạt động trên nền UNIX và Microsoft Windows
• Các chức năng hỗ trợ:
– Bắt đầu chương trình, xác định những yếu tố làm ảnh
hưởng đến hoạt động của chương trình
– Dừng chương trình với điều kiện biết trước
– Khi chương trình bị dừng, kiểm tra những gì đã xảy ra
– Thay đổi các lệnh trong chương trình để LTV có thể thử
nghiệm gỡ rối từng lỗi một
2.8. Tập trung vào các lệnh mới viết /
mới viết lại
• Kiểm tra sự thay đổi mới nhất
– Lỗi thường xảy ra ở những đoạn CT mới được bổ sung
– Nếu phiên bản cũ OK, phiên bản mới có lỗi => lỗi chắc
chắn nằm ở những đoạn CT mới
– Lưu ý, khi sửa đổi, nâng cấp : hãy giữ lại phiên bản cũ –
đơn giản là comment lại đoạn mã cũ
– Đặc biệt, với các hệ thống lớn, làm việc nhóm thì việc sử
dụng các hệ thống quản lý phiên bản mã nguồn và các
cơ chế lưu lại quá trình sửa đổi là vô cùng hữu ích (
source safe )
Tập trung vào các lệnh mới viết / mới
viết lại
• Các lỗi xuất hiện thất thường :
– Khó giải quyết
– Thường gán cho lỗi của máy tính, hệ điều hành …
– Thực ra là do thông tin của chính CT : không phải do thuật
toán, mà do thông tin bị thay đổi qua mỗi lần chạy
– Các biến đã đc khởi tạo hết chưa ?
– Lỗi cấp phát bộ nhớ ? Vd :
char *vd( char *s) {
char m[101];
strncpy(m,s,100)
return m;
}
– Giải phóng bộ nhớ động ?
– for (p=listp; p!=NULL; p=p->next) free(p) ; ???
Tập trung vào các lệnh mới viết / mới viết lại
• Phải gỡ rối ngay, không nên để sau
– Khó: Viết toàn bộ chương trình; kiểm tra toàn bộ
chương trình, gỡ rối toàn bộ chương trình
– Dễ hơn: Viết từng đoạn, kiểm tra từng đoạn, gỡ rối
• Nên giữ lại các phiên bản trước
từng đoạn; viết từng đoạn, kiểm tra từng đoạn, gỡ rối
từng đoạn;
– Khó: Thay đổi mã nguồn, đánh dấu các lỗi; cố gắng nhớ
xem đã thay đổi cái gì từ lần làm việc trước
– Dễ hơn: Backup mã nguồn, thay đổi mã nguồn, đánh
dấu các lỗi; so sánh phiên bản mới với phiên bản cũ để
xác định các điểm thay đổi
Giữ lại các phiên bản trước đó
• Cách 1: Sao chép bằng tay vào 1 thư mục
…
$ mkdir myproject
$ cd myproject
Create project files here.
$ cd ..
$ cp –r myproject myprojectDateTime
$ cd myproject
Continue creating project files here.
…
• Lặp lại mỗi lần có phiên bản mới
• Cách 2: dùng công cụ như RCS (Revision Control
System), CVS, v.v.
Tóm lại
• Gỡ rối là 1 nghệ thuật mà ta phải luyện tập
thường xuyên
• Nhưng đó là nghệ thuật mà ta không muốn
• Mã nguồn viết tốt có ít lỗi hơn và dễ tìm hơn
• Đầu tiên phải nghĩ đến nguồn gốc sinh ra lỗi
• Hãy suy nghĩ kỹ càng, có hệ thống để định vị khu
vực gây lỗi
• Không gì bằng học từ chính lỗi của mình – điều
này càng đúng đối với LTV
• Trường hợp gặp EOF
for (i=0; i
• Các trường hợp khác?
This is wrong.
Why?
– Nearly full
– Exactly full
– Over full
Ví dụ
• Rewrite yet again
s[i] = '\0';
for (i=0; ; i++) {
int c = getchar();
if (c==EOF || c=='\n' || i==MAXLINE-1) {
break;
}
else s[i] = c;
}
There’s still a problem...
Where’s
the ‘d’?
Input:
Four
score and seven
years
Output:
FourØ
score anØ
sevenØ
yearsØ
Ambiguity in Specification
• Nếu dòng quá dài, xử lý thế nào?
– Giữ MAXLINE ký tự đầu, bỏ qua phần còn lại?
– Giữ MAXLINE ký tự đầu + ‘\0’, bỏ qua phần còn lại?
– Giữ MAXLINE ký tự đầu+’\0’, lưu phần còn lại cho lần gọi
sau của input function?
• Có thể phần đặc tả - specification không hề đề cập khi
MAXLINE bị quá
– Có thể người ta không muốn dòng dài quá giới hạn trong
mọi trường hợp
– Đạo đức: kiểm tra đã phát hiện ra một vấn đề thiết kế,
thậm chí có thể là một vấn đề đặc điểm kỹ thuật !
• Quyết định phải làm gì
– Cắt những dòng quá dài?
– Lưu phần còn lại để đọc như 1 dòng mới?
Kiểm tra điều kiện trước và sau
• Xác định những thuộc tính cần đi trước (điều kiện trước) và
sau (điều kiện sau) để mã nguồn được thi hành
• Ví dụ: các giá trị đầu vào phải thuộc 1 phạm vi cho trước
double avg( double a[], int n) {
int i; double sum=0.0;
for ( i = 0; i
sum+=a[i];
return sum/n;
• Nếu n=0 ?, nếu n<0 ?
}
• Có thể thay : return n <=0 ? 0.0: sum/n;
b. Statement Testing
• “Testing để thỏa mãn điều kiện rằng
mỗi lệnh trong 1 CT phải thực hiện ít
nhất 1 lần khi testing.”
Ví dụ
• Statement testing pseudocode:
Statement testing:
Phải chắc chắn các lệnh “if”
và 4 lệnh trong các nhánh
phải được thực hiện
if (condition1)
statement1;
else
statement2;
if (condition2)
statement3;
else
statement4;
…
– condition1 là đúng và condition2 là đúng
• Thực hiện statement1 và statement3
– condition1 là sai và condition2 là sai
• Thực hiện statement2 và statement4
• Đòi hỏi 2 tập dữ liệu. Ví dụ:
c. Path Testing
• Kiểm tra để đáp ứng các tiêu chuẩn đảm bảo
rằng mỗi đường dẫn logic xuyên suốt chương
trình được kiểm tra. Thường thì đường dẫn xuyên
suốt chương trình này được nhóm thành một tập
hữu hạn các lớp. Một đường dẫn từ mỗi lớp sau
đó được kiểm tra.
• Khó hơn nhiều so với statement testing
– Với các CT đơn giản, có thể liệt kê các nhánh đường dẫn
xuyên suốt code
– Ngược lại, bằng các đầu vào ngẫu nhiên tạo các đường
dẫn theo CT
Ví dụ
Path testing:
Cần đảm bảo tất cả các luồng
công việc đều được kiểm thử
if (condition1)
statement1;
else
statement2;
…
if (condition2)
statement3;
else
statement4;
…
• Đòi hỏi 4 tập dữ liệu:
– condition1 là true và condition2 là true
– condition1 là true và condition2 là false
– condition1 là false và condition2 là true
– condition1 là false và condition2 la false
• Chương trình thực tế => bùng nổ các tổ hợp!!!
Bài tập
1
F
else
A>0
T
3
2
B>0
T
F
(1) input(A,B)
if (A>0)
(2) Z = A;
(3) Z = 0;
if (B>0)
(4) Z = Z+B;
(5) output(Z)
4
5
What is the path condition for path <1,2,5>?
(A>0) Л (B0)
Bài tập
1
A>B
T
F
2
F
B<0
T
4
3
(1) input(A,B)
if (A>B)
(2) B = B*B;
if (B<0)
(3) Z = A;
else
(4) Z = B;
(5) output(Z)
5
What is the path condition for path <1,2,3,5>?
(A>B) Л (B<0)
Consider ANOTHER example…
1
A>B
T
F
F
2
T
B<0
T
4
3
5
What is the path condition for path <1,2,3,5>?
(A>B) Л (B<0) (B2<0) = FALSE
(1) input(A,B)
if (A>B)
(2) B = B*B;
if (B<0)
(3) Z = A;
else
(4) Z = B;
(5) output(Z)
d. Stress Testing
• “Tiến hành thử nghiệm để đánh giá một hệ thống
hay thành phần tại hoặc vượt quá các giới hạn
của các yêu cầu cụ thể của nó”
• Phải tạo:
– Một tập lớn đầu vào - Very large inputs
– Các đầu vào ngẫu nhiên - Random inputs (binary vs.
• Nên dùng máy tính để tạo đầu vào
ASCII)
Ví dụ: Stress Testing
• Example program:
Stress testing: Phải cung cấp
các đầu vào ngầu nhiên dạng
nhị phân hay ASCII
#include
int main(void) {
char c;
while ((c = getchar()) != EOF)
putchar(c);
return 0;
}
• Mục tiêu: Copy tất cả các ký tự từ stdin vào stdout; nhưng
lưu ý bug!!!
• Làm việc với tập dữ liệu ASCII chuẩn ( tự tạo)
• Máy tính tự tạo ngẫu nhiên tập dữ liệu dạng 255 (decimal),
hay 11111111 (binary), và EOF để dừng vòng lặp
Ví dụ: Stress Testing
Stress testing: Phải cung cấp
very large inputs
• Example program:
#include
int main(void) {
short charCount = 0;
while (getchar() != EOF)
charCount++;
printf("%hd\n", charCount);
return 0;
}
• Mục tiêu: Đếm và in số các kỹ tự trong stdin
• Làm việc với tập dữ liệu có kích thước phù hợp
• Sẽ có lỗi với tập dữ liệu do máy tạo chứa hơn
32767 characters
3. Internal Testing
• Internal testing: Thiết kế chương trình để nó tự
kiểm thử chính nó
• Internal testing techniques
conservation properties
(1) Kiểm tra các giá trị bất biến - Testing invariants
(2) Kiểm tra các thuộc tính cần được bảo lưu - Verifying
(3) Kiểm tra các giá trị trả về - Checking function return
values
(4) Thay đổi mã nguồn tạm thời - Changing code
temporarily
(5) Giữ lại phần mã kiểm tra - Leaving testing code intact
a. Testing Invariants
• Kiểm tra các điều kiện tiên nghiệm và điều kiện
hậu nghiệm
• 1 số khía cạnh của CTDL là bất biến, vì vậy, khi 1
hàm tác động lên CTDL, phải kiểm tra tính bất
biến của CTDL trước và trong khi tác động.
• Ví dụ: hàm thêm vào danh sách nối kép
– Duyệt danh sách nối kép
– Nếu node x trỏ đến node y, vậy node y có nhất định phải
• Ví dụ: hàm thêm nút vào cây nhị phân tìm kiếm
– Duyệt cây
– Sau khi thêm các nút còn được sắp đúng thứ tự không ?
trỏ đến node x hay không ?
b. Kiểm tra các đặc tính cần được bảo
lưu
• Khái quát hóa của testing invariants
• 1 hàm cần kiểm tra các cấu trúc dữ liệu bị tác
động tại các điểm đầu và cuối
• VD: hàm Str_concat()
• VD: Hàm chèn thêm PT vào danh sách -List
insertion function
– Tại điểm khởi đầu, tính độ dài ds
– Tại điểm cuối, Tính độ dài mới
– Độ dài mới = độ dài cũ + 1?
– Tại điểm đầu, tìm độ dài của 2 xâu đã cho; tính tổng
– Tại điểm cuối, tìm độ dài của xâu kết quả
– 2 độ dài có bằng nhau không ?
c. Kiểm tra các giá trị trả về
• Java/C++:
– Các phương thức phát hiện lỗi có thể bỏ qua 1 trường
hợp ngoại lệ đã được biết đến (“throw a checked
exception”)
– Việc gọi 1 phương thức cần phải kiểm soát được ngoại lệ
• C:
(hoặc lại bỏ qua ngoại lệ đó)
– Không có kỹ thuật kiểm soát các trường hợp ngoại lệ
– Hàm phát hiện ra lỗi thông qua giá trị trả về
– LTV thường quên kiểm tra giá trị trả về nên kiểm tra
giá trị trả về
Kiểm tra các giá trị trả về
Bad code
Good code
– scanf() trả về số giá trị đã đọc
int i;
scanf("%d", &i);
int i;
if (scanf("%d", &i) != 1)
/* Error */
– printf() có thể bị lỗi nếu ghi vào file hay đĩa đầy; trả
về số các ký tự (ko phải giá trị) đã ghi
Bad code??? Good code, or overkill???
int i = 100;
printf("%d", i);
int i = 100;
if (printf("%d", i) != 3)
/* Error */
d. Thay đổi mã nguồn tạm thời
• Thay đổi mã nguồn tạm thời để kiểm tra các điều kiện
biên và kiểm tra trực tiếp các giá trị
• Ví dụ: chương trình sắp xếp trên mảng:
– Dùng mảng với kích thước nhỏ
– CT có kiểm soát được lỗi tràn bộ nhớ hay không ?
• Nhớ áp dụng vào bài tập
• Ví dụ: dùng bảng băm
– Tạm thời cho hàm băm trả về kết quả là 1 hằng số
– Như vậy toàn bộ các giá trị cần băm sẽ có cùng 1 chỉ số,
tức là số lượng các giá trị cần băm có cùng chỉ số là rất lớn
– CT có kiểm soát được tình huống này hay không ?
Thay đổi mã nguồn tạm thời
• Viết 1 phiên bản hàm cấp phát bộ nhớ và phát
hiện ra lỗi sớm, để kiểm thử đoạn mã nguồn bị lỗi
thiếu bộ nhớ:
void *testmalloc( size_t n) {
static int count =0;
if (++count > 10)
return NULL;
else
return malloc(n);
}
e. Giữ lại phần mã kiểm tra
• Để nguyên phần mã kiểm tra quan trọng trong mã nguồn
• Có thể khoanh vùng chúng lại như là mã gỡ rối:
#ifndef NDEBUG … #endif
– Kích hoạt/ tắt các assert macro
– Kích hoạt/ tắt các đoạn mã gỡ rối của LTV
• Khi dịch thì dùng tùy chọn –DNDEBUG của gcc
– Việc viết các đoạn mã tự kiểm tra cho CT có thể làm giảm chi phí
bảo trì chương trình
– Mã nguồn sáng sủa cũng làm giảm chi phí bảo trì chương trình
– Nhưng: việc viết các đoạn mã tự kiểm tra khắp nơi sẽ làm
• Đề phòng xung đột:
giảm tính rõ ràng của chương trình
4. Các chiến lược kiểm thử tổng quát
• Kiểm thử dần dần -Testing incrementally
• So sánh các cách cài đặt khác nhau -Comparing
implementations
• Tự động kiểm thử – Automation
• Kiểm thử theo lỗi - Bug-driven testing
• Tiêm, gài lỗi - Fault injection
a. Testing Incrementally
• Test khi viết code
– Thêm tests khi tạo 1 lựa chọn mới - new cases
– Test phần đơn giản trước phần phức tạp
– Test units (tức là từng module riêng lẻ) trước khi testing
• Thực hiện regression testing – kiểm thử hồi quy
– Xử lý đc 1 lỗi thường tạo ra những lỗi mới trong 1 he
toàn hệ thống
thống lớn, vì vậy …
– Phải đảm bảo chắc chắn hệ thống không “thoái lui” kiểu
như chức năng trước kia đang làm việc giờ bị broken,
nên…
– Test mọi khả năng để so sanh phiên bản mới với phiên
bản cũ
Testing Incrementally (cont.)
• Tạo “giàn giáo” - scaffolds và “mẫu” -stubs để
test đoạn code mà ta quan tâm
Hàm gọi đến code
mà ta quan tâm
Đoạn code cần quan tâm
Hàm được gọi
bởi đoạn code cần
quan tâm
Hàm được gọi
bởi đoạn code cần
quan tâm
b. So sánh các cài đặt
• Hãy chắc chắn rằng các triển khai độc lập hành
xử như nhau
• Ví dụ: So sánh hành vi của chương trình mà bạn
dịch ( TB C++3.0 ) với GCC
• Ví dụ: So sánh hành vi của các hàm bạn tạo
trong str.h với các hàm trong thư viện string.h
• Trường hợp có thể tính 1 kết quả bẳng 2 cách
khác nhau (2 thuật toán giải quyết cùng 1 bài
toán): xây dựng 2 chương trình và kiểm tra kết
quả thực hiện
– Kết quả giống nhau: 2 chương trình cùng đúng
– Khác nhau: ít nhất 1 trong 2 chương trình bị sai
c. Tự động kiểm thử
• Kiểm thử thủ công rất nặng nhọc, tốn kém và nhàm
chán thậm chí không đủ độ tin cậy.
• Trong quá trình kiểm thử, phải
– Thực hiện lặp đi lặp lại các thao tác kiểm thử
– Dùng nhiều bộ dữ liệu nhập
– Nhiều lần so sánh dữ liệu đầu ra
• Tạo testing code để tránh các nhược điểm nói trên
– Viết 1 bộ kiểm thử để kiểm tra toàn bộ chương trình mỗi
khi có sự thay đổi, sau khi biên dịch thành công
– Cần biết cái gì được chờ đợi
• Tạo ra các đầu ra, sao cho dễ dàng nhận biết là đúng hay sai
– Tự động kiểm thử tốt hơn nhiều so với kiểm thử thủ công
Tự động hóa kiểm thử hồi quy
• Tuần tự so sánh kết quả thực hiện các phiên bản mới
với kết quả thực hiện những phiên bản cũ tương ứng.
• Mục đích : đảm bảo việc sửa lỗi sẽ không làm ảnh
hưởng những phần khác trừ khi chúng ta muốn
• 1 số hệ thống có công cụ trợ giúp kiểm thử tự động :
– Ngôn ngữ scripts : cho phép viết các đoạn script để test
tuần tự
– Unix : có các thao tác trên tệp tin như cmp và diff để so
sanh dữ liệu xuất, sort sắp xếp các phần tử, grep để kiểm
thử dữ liệu xuất, wc, sum va freq để tổng kết dữ liệu xuất
• Khi kiểm thử hồi quy, cần đảm bảo phiên bản cũ là
đúng, nếu sai thì rất khó xác định và kết quả sẽ
không chính xác
• Cần phải kiểm tra chính việc kiểm thử hồi quy 1 cách
định kỳ để đảm bảo nó vẫn hợp lệ
Tạo ra những kiểm thử độc lập
• Kiểm thử độc lập với các giá trị nhập và giá trị xuất mong đợi sẽ bổ
xung cho kiểm thử hồi quy
• VD: Dùng ngôn ngữ NewAwk thực hiện kiểm thử 1 CT ngắn, ghi dữ
liệu đẩu ra của CT vào 1 tệp, ghi kết quả đúng vào 1 tệp khác rồi so
sánh 2 tệp, nếu khác nhau thì thông báo lỗi
echo 3 5 | newawk ‘{i=1; print ($si)++; print $i ,i}’ > out1
echo ‘3 4 1’ > out2 #kết quả đúng
if ! Cmp –s out1 out2 # nếu kq so sánh khác nhau
then
echo ‘BAD: test failed’
Fi
• Mọt lỗi có thể cần nhiều thao tác kiểm thử hoặc phải kiểm tra toàn bộ
các lớp mới, hoặc có thể thêm vào những đoạn CT bảo vệ để có thể
bắt đc những lỗi trong CT
d. Bug-Driven Testing
• Tìm thấy 1 bug
Ngay lập tức tạo 1 test để bắt lỗi
• Đơn giản hóa việc kiểm thử hồi quy
e. Fault injection
• Chủ động (tạm thời) cài các bugs!!!
• Rồi quyết định nếu tìm thấy chúng
• Kiểm thử chính đoạn mã kiểm thử/ quy trình
kiểm thử
4. Ai kiểm thử cái gì
• Programmers: White-box testing
– Thuận lợi: Người triển khai nắm rõ mọi luồng dữ liệu
– Khó khăn: Bị ảnh hưởng bởi cách thiết kế CT hay viết
• Quality Assurance (QA) engineers: Black-box
testing
– Thuận lợi: Không có khái niệm về implementation
– Khó khăn: Không muốn test mọi logical paths
• Customers: Field testing
– Thuận lợi: Có các cách sử dụng CT bất ngờ; dễ gây lỗi
– Khó khăn: Không đủ trường hợp; khách hàng không
mã nguồn
thích tham gia vào quá trình test
Các kỹ thuật kiểm thử
• Black-Box: Testing chỉ dựa trên việc
phân tích các yêu cầu - requirements
(unit/component specification, user
documentation, v.v.). Còn được gọi là
functional testing.
• White-Box: Testing dựa trên việc
phân tích các logic bên trong - internal
logic (design, code, v.v.). (Nhưng kết
quả mong đợi vẫn đến từ
requirements.) Còn đc gọi là structural
testing.
Các mức độ kiểm thử
• Unit: testing các mẫu công việc nhỏ nhất của LTV để có thể
lập kế hoạch và theo dõi hợp lý (vd : function, procedure,
module, object class, ….)
• Component: testing 1 tập hợp các units tạo thành 1 thành
phần (vd : program, package, task, interacting object
classes, …)
• Product: testing các thành phần tạo thành 1 sản phẩm (
subsystem, application, …)
– Bắt đầu = functional (black-box) tests,
– Rồi thêm = structural (white-box) tests, và
– Tiến hành từ unit level đến system level với 1 hoặc một vài bước
tích hợp
• System: testing toàn bộ hệ thống
• Testing thường:
Tại sao không "test everything"?
20 x 4 x 3 x 10 x 2 x 100 = 480,000 tests
Chi phí cho 'exhaustive' testing:
Nếu 1 giây cho 1 test, 8000 phút, 133 giờ, 17.7 ngày
(chưa kể nhầm lẫn hoặc test đi test lại)
nếu 10 secs = 34 wks, 1 min = 4 yrs, 10 min = 40 yrs
Kiểm thử bao nhiêu thì đủ?
• Không bao giờ đủ !
• Khi bạn thực hiện những test mà bạn đã lên kế hoạch
• Khi khách hàng/người sử dụng thấy thỏa mãn
• Khi bạn đã chứng minh đc rằng hệ thống hoạt động
đúng, chính xác
• Khi bạn tin tưởng rằng HT hoạt động tốt
• Phụ thuộc vào các rủi ro có thể xảy ra với hệ thống
– Thời gian để test luôn có giới hạn
– Dùng RISK để xác định:
• Cái gì phải test trước
• Cái gì phải test nhiều
• Mỗi phần tử cần test kỹ đến mức nào?
• Tại một thời điểm nhất định, cái gì không cần test
IV. GỠ RỐI
1. Tổng quan về gỡ rối chương trình
2. Tìm kiếm và gỡ rối
Mở đầu
• Gỡ rối chương trình lớn là công việc không đơn
giản
• 1 LTV giỏi phải biết
• Debuggers
• Version control systems
– Nhiều chiến lược gỡ rối khác nhau
– Các công cụ hỗ trợ việc gỡ rối
1. Tổng quan về gỡ rối chương trình
• Gỡ rối là gì ?
– Khi chương trình bị lỗi, gỡ rối là các công việc cần làm để làm cho chương trình dịch
thông, chạy thông
– Thật không may, gỡ rối luôn là thao tác phải làm khi lập trình, thao tác này rất tốn
kém
Cách tốt nhất vẫn là phòng ngừa
•
– Khi bắt đầu gỡ rối chương trình, bạn đã biết là chương trình không chạy.
– Nếu bạn biết lý do tại sao chương trình không chạy, bạn có thể sửa được chương trình
cho nó chạy
– Nếu bạn hiểu chương trình của bạn, bạn sẽ có ít sai lầm và dễ dàng sửa chữa sai sót
hơn. Bí quyết là viết mã đơn giản, hiệu quả, chú thích hợp lý.
• Đối với mã nguồn, tiêu chí nào quan trọng hơn: rõ ràng hay chính xác ?
– Nếu mã nguồn rõ ràng, bạn có thể làm cho chương trình trở nên chính xác.
– Bạn có chắc là làm cho chương trình trở nên chính xác nếu nó không rõ ràng hay
không ?
• Nếu chương trình được thiết kế với cấu trúc tốt, được viết bằng phong cách lập
trình tốt và áp dụng các kỹ thuật viết chương trình hiệu quả, bẫy lỗi thì chi phí
cho việc gỡ rối sẽ được giảm thiểu.
2. Tìm kiếm và gỡ rối
• Khi có lỗi, ta thường đổ cho trình dịch, thư viện hay bất cứ nguyên
nhân khách quan nào khác …tuy nhiên, cuối cùng thì lỗi vẫn là lỗi của
CT, và trách nhiệm gỡ rối thuộc về LTV
Phải hiểu vấn đề xuất phát từ đâu thì mới giải quyết được:
•
– Lỗi xảy ra ở đâu? Hầu hết các lỗi thường đơn giản và dễ tìm. Hãy khảo sát các
đầu mối và cố gắng xác định được đoạn mã nguồn gây lỗi
– Lỗi xảy ra như thế nào? Khi đã có 1 số thông tin về lỗi và nơi xảy ra lỗi, hãy
suy nghĩ xem lỗi xảy ra như thế nào
– Đâu là nguyên nhân gây lỗi? Suy luận ngược trở lại trạng thái của CT để xác
định nguyên nhân gây ra lỗi
• Gỡ rối liên quan đến việc suy luận lùi, giỗng như phá án.
1 số vấn đề không thể xảy ra và chỉ có những thông tin xác thực mới
đáng tin cậy
Phải đi ngược từ kết quả để khám phá nguyên nhân,
Khi có lời giải thích đầy đủ, ta sẽ biết được vấn đề cần sửa và có thể
phát hiện ra 1 số vấn đề khác
2. Tìm kiếm và gỡ rối
Debugging Heuristic Áp dụng khi nào
(2) Nghĩ trước khi viết lại chương trình
(1) Hiểu các thông báo lỗi (error messages) Build-time (dịch)
(3) Tìm kiếm các lỗi (bug) hay xảy ra
(4) Divide and conquer Run-time (chạy)
(5) Viết thêm các đoạn mã kiểm tra để
chương trình tự kiểm tra nó
(6) Hiện thị kết quả
(7) Sử dụng debugger
(8) Tập trung vào các lệnh mới viết / mới viết
lại
2.1. Hiểu các thông báo lỗi
Gỡ rối khi dịch (build-time) CT dễ hơn gỡ rối khi
chạy CT nếu LTV hiểu được các thông báo lỗi
Một số thông báo lỗi đến từ preprocessor
Gõ sai tên file cần gọi
Thiếu dấu */
#include
int main(void)
/* Print "hello, world" to stdout and
return 0.
{
printf("hello, world\n");
return 0;
}
$ gcc217 hello.c -o hello
hello.c:1:20: stdioo.h: No such file or directory
hello.c:3:1: unterminated comment
hello.c:2: error: syntax error at end of input
Hiểu các thông báo lỗi
• Một số thông báo lỗi đến từ compiler
#include
int main(void)
/* Print "hello, world" to stdout and
return 0. */
{
printf("hello, world\n")
retun 0;
}
Hiểu các thông báo lỗi
• Một số thông báo lỗi đến từ linker
#include
int main(void)
/* Print "hello, world" to stdout and
return 0. */
{
prinf("hello, world\n")
return 0;
}
2.2. Nghĩ trước khi viết lại chương
trình
Việc thay đổi mã nguồn không hợp lý có thể gây ra
nhiều vấn đề hơn là để nguyên không thay đổi gì,
nên…
Phải nghĩ trước khi làm
Nghĩ trước khi viết lại chương trình
• Gỡ rối ngay khi gặp
– Khi phát hiện lỗi, hãy sửa ngay, đừng để sau mới sửa, vì có thể lỗi
không xuất hiện lại (do tình huống)
– Cân nhắc: việc sửa chữa này có ảnh hưởng tới các tình huống khác
hay không ?
• Quan sát lỗi từ góc độ khác
– Viết đoạn mã nguồn gây lỗi ra giấy
• Đừng chép hết cả đoạn không có nguy cơ gây lỗi, hoặc in toàn bộ
code ra giấy in => phá vỡ cây cấu trúc
– Vẽ hình minh họa các cấu trúc dữ liệu
• Nếu mà giải thuật làm thay đổi CTDL, vẽ lại hình trước khi viết lại
giải thuật
– Đọc trước khi gõ vào
• Đừng vội vàng, khi không rõ điều gì thực sự gây ra lỗi và sửa
không đúng chỗ sẽ có nguy cơ gây ra lỗi khác
Nghĩ trước khi viết lại chương trình
• Tạm dừng viết CT
– Khi gặp vấn đề, khó khăn, chậm tiến độ, lập tức thay đổi công việc =>
rút ra khỏi luồng quán tính sai lầm …
– Bỏ qua đoạn CT có lỗi
– Khi nào cảm thấy sẵn sàng thì chữa
• Giải thích logic của đoạn mã nguồn:
– Cho chính bạn
• Tạo điều kiện để suy nghĩ lại
– Cho ai khác có thể phản bác
• Extrem programming : làm việc theo cặp, pair programming,
người này LT, người kia kiểm tra, và ngược lại
– Cho cái gì đó không thể phản bác (cây, cốc trà đá, gấu bông, cún,…)
• Tạo điều kiện củng cố suy luận của mình
2.3. Tìm kiếm các lỗi hay xảy ra
• Xem lại các quy tắc lập trình
int i;
…
scanf("%d", i);
char c;
…
c = getchar();
switch (i) {
case 0:
…
/* missing break */
case 1:
…
break;
…
}
if (i = 5)
…
while (c = getchar() != EOF)
…
if (5 < i < 10)
…
if (i & j)
…
Chú ý: nếu đặt chế độ
cảnh báo (warnings) khi
dịch thì hầu hết các lỗi
kiểu này sẽ được phát hiện
Tìm kiếm các lỗi hay xảy ra
• Khi gặp vấn đề, hãy liên tưởng đến những trường
hợp tương tự đã gặp
– Vd1 :
int n; scanf(“%d”,n); ?
– Vd2 :
int n=1; double d=PI;
printf(“%d %f \n”,d,n); ??
• Không khởi tạo biến (với C) cũng sẽ gây ra những
lỗi khó lường.
Tìm kiếm các lỗi hay xảy ra
• Làm cho lỗi xuất hiện lại
– Cố gắng làm cho lỗi có thể xuất hiện lại khi cần
– Nếu không được, thì thử tìm nguyên nhân tại sao lại
không làm cho lỗi xuất hiện lại
Các phương pháp gỡ rối
• Tránh mắc cùng 1 lỗi 2 lần : Sau khi sửa 1 lỗi, hãy suy nghĩ
xem có lỗi tương tự ở nơi nào khác không. VD :
break;
case ‘o’ : /* tên tệp ouput*/
outname = argv[i]; break;
case ‘f’ : from = atoi(argv[i]); break;
case ‘t’ : to = atoi(argv[i]); break;
for (i=1;i
2.4. Divide and Conquer
• Thu hẹp phạm vi
• Tập trung vào dữ liệu gây lỗi
Divide and Conquer
– Thử chương trình với các tham số đầu vào từ đơn giản đến phức
tạp, từ nhỏ đến lớn để tìm lỗi
– Ví dụ: chương trình lỗi với file đầu vào filex
• Tạo ra phiên bản copy của filex , tên là filexcopy
• Xoá bớt nửa sau của filexcopy
• Chạy chương trình với tham số đầu vào là filexcopy
– Nếu chạy thông => nửa đầu của filex không gây lỗi, loại bỏ nửa này,
tìm lỗi trong nửa sau của filex
– Nếu không chạy => nửa đầu của filex gây lỗi, loại bỏ nửa sau, tìm lỗi
trong nửa đầu của filex
• Lặp cho đến khi không còn dòng nào trong filex có thể bị loại bỏ
– Cách khác: bắt đầu với 1 ít dòng trong filex, thêm dần các dòng
vào cho đến khi thấy lỗi
• Khử đầu vào
Divide and Conquer
• Khử mã nguồn
– Thử chương trình với các đoạn mã nguồn từ ngắn đến
dài để tìm lỗi
– Example: đoạn chương trình có sử dụng đến các phần
• Viết riêng từng lời gọi hàm trong đoạn chương trình bị lỗi
(test client)
– Hoặc viết thêm phần kiểm tra gọi hàm vào phần CTC được gọi
• Chạy thử test client
• Không chạy => lỗi liên quan đến việc gọi/ thực hiện CTC
vừa thử
• Chạy thông => lỗi nằm trong phần còn lại, tiếp tục thử gọi
các hàm khác
CTC khác không chạy
2.5. Viết thêm các đoạn mã kiểm tra để
chương trình tự kiểm tra nó
• Dùng internal test để khử lỗi trong CT và giảm
nhẹ công việc tìm kiếm lỗi
– Chỉ cần viết thêm 1 hàm để kiểm tra, gắn vào trước và
sau đoạn có nguy cơ, comment lại sau khi đã xử lý xong
lỗi
đã học
• Kiểm tra các giá trị không thay đổi
• Kiểm tra các đặc tính cần bảo lưu
• Kiểm tra giá trị trả về của hàm
• Thay đổi mã nguồn tạm thời
• Kiểm tra mà không làm thay đổi mã nguồn
• Dùng assertion để nhận dạng các lỗi có trong CT
– Các kỹ thuật viết thêm mã tự kiểm tra cho chương trình
Assertions
• Assertion: 1 macro hay 1 CT con dùng trong quá trình
phát triển ứng dụng, cho phép CT tự kiểm tra khi
chạy.
• Return true >> OK, false >> có 1 lỗi gì đó trong CT.
• Ghi lại những giả thiết được đưa ra trong code
• Loại bỏ những điều kiện không mong đợi
• VD :
– Nếu hệ thống cho rằng file dữ liệu về khách hàng không
bao giờ vượt quá 50 000 bản ghi, CT có thể chứa 1
assertion rằng số bản ghi là <= 50 000.
– Khi mà số bản ghi <= 50,000, assertion sẽ không có phản
ứng gì.
– Nếu đếm đc hơn 50 000 bản ghi, nó sẽ lớn tiếng “khẳng
định” rằng có 1 lỗi trong CT
Assertions
• Assertions có thể được dùng để kiểm tra các giả thiết
như :
– Các tham số đầu vào nằm trong phạm vi mong đợi (tương
tự với các tham số đầu ra)
– File hay stream đang được mở (hay đóng) khi 1 CTC bắt
đầu thực hiện (hay kết thúc)
– 1 file hay stream đang ở bản ghi đầu tiên (hay cuối cùng)
khi 1 CTC bắt đầu ( hay kết thúc) thực hiện
– 1 file hay stream được mở để đọc, để ghi, hay cả đọc và ghi
– Giá trị của 1 tham số đầu vào là không thay đổi bởi 1 CTC
– 1 pointer là non-NULL
– 1 mảng đc truyền vào CTC có thể chứa ít nhất X phần tử
– 1 bảng đã đc khởi tạo để chứa các giá trị thực
– 1 danh sách là rỗng (hay đầy) lkhi 1 CTC bắt đầu (hay kết
thúc) thực hiện
Assertions
• End users không cần thấy
các thông báo của assertion ;
• Assertions chủ yếu đc dùng
trong quá trình phát triển
hay bảo dưỡng ứng dụng.
• Dịch thành code khi phát
triển, loại bỏ khỏi code trong
sản phẩm để nâng cao hiệu
năng của chương trình
• Rất nhiều NNLT hỗ trợ
assertions : C++, Java và
Visual Basic.
VD:
#define ASSERT(condition, message)
{
if ( !(condition) ) {
fprintf(
stderr,
"Assertion %s failed: %s\n",
condition,
message);
exit( EXIT_FAILURE );
}
}
• Kể cả khi NNLT không hỗ trợ,
thì cũng có thể dễ dàng xây
dựng
Dùng assertions như thế nào ?
• Bẫy lỗi cho những tình huống lường trước (sự
kiện ta chờ đợi sẽ xảy ra);
– Error-handling : checks for bad input data Hướng tới
• Dùng assertions cho các tình huống không lường
trước (sự kiện không mong đợi xảy ra hoặc không
bao giờ xảy ra)
– Assertions : check for bugs in the code hướng đến
việc xử lý lỗi
• Tránh đưa code xử lý vào trong assertions
– Điều gì xảy ra khi ta turn off the assertions ?
việc hiệu chỉnh chương trình, tạo ra phiên bản mới của
chương trình
Dùng assertions như thế nào ?
• Các chương trình lớn:
– trước tiên xác nhận lỗi (dùng assertion),
– sau đó bẫy lỗi (dùng error-handling)
• Nguyên nhân gây lỗi đã được xác định:
– hoặc dùng assertion, hoặc dùng error-handling,
– không dùng cả 2 cùng lúc
• Các chương trình cực lớn, nhiều người cùng phát triển
trong thời gian 5-10 năm, hoặc hơn nữa ?
– Cả assertions và error handling code có thể đc dùng cho
cùng 1 lỗi.
– Ví dụ trong source code cho Microsoft Word, những điều
kiện luôn trả về true thì đc dùng assertion, nhưng đồng thời
cũng đc xử lý.
– Assertions rất có lợi vì nó giúp loại bỏ rất nhiều lỗi trong
quá trình phát triển hệ thống
Kiểm tra các giá trị không thay đổi
• Có thể sử dụng assert để kiểm tra các giá trị không thay đổi
#ifndef NDEBUG
int isValid(MyType object) {
…
Test invariants here.
Return 1 (TRUE) if object passes
all tests, and 0 (FALSE) otherwise.
…
}
#endif
void myFunction(MyType object) {
assert(isValid(object));
…
Manipulate object here.
…
assert(isValid(object));
}
2.6. Hiện thị kết quả đầu ra
•
In giá trị của các biến tại các
điểm có khả năng gây lỗi để
định vị khu vực gây lỗi, hoặc
• Xác định tiến trình thực hiện :
“đến đây 1”
• Poor:
stdout is buffered;
CT có thể có lỗi
trước khi hiện ra
output
printf("%d", keyvariable);
• Maybe better:
printf("%d\n", keyvariable);
• Better:
In '\n' sẽ xóa bộ nhớ
đệm stdout , nhưng
sẽ không xóa khi in
ra file
Gọi fflush() để làm
sạch buffer 1 cách
tường minh
printf("%d", keyvariable);
fflush(stdout);
Hiện thị kết quả đầu ra
• Tạo log file
• Lưu vết
– Giúp ghi nhớ đc các vấn đề đã xảy ra,
và giải quyết các vđề tương tự sau
này, cũng như khi chuyển giao CT
cho người khác..
• Maybe even better:
In debugging
output ra stderr;
debugging output
có thể tách biệt
với đầu ra thông
thường bằng cách
in ấn của CT
fprintf(stderr, "%d", keyvariable);
• Maybe better still:
Ghi ra 1 a log file
Ngoài ra: stderr
không dùng buffer
FILE *fp = fopen("logfile", "w");
…
fprintf(fp, "%d", keyvariable);
fflush(fp);
2.7. Sử dụng trình gỡ rối (debugger)
IDE : kết hợp soạn thảo, biên dịch, gỡ rối …
•
• Các trình gỡ rối với giao diện đồ họa cho phép chạy chương
trình từng bước qua từng lệnh hoặc từng hàm, dừng ở những
dòng lệnh đặc biệt hay khi xuất hiện những đk đặc biệt, bên
canh đó có các công cụ cho phép định dạng và hiển thị giá trị
các biến, biểu thức
• Trình gỡ rối có thể được kích hoạt trực tiếp khi có lỗi hoặc gắn
vào chương trình đang chạy.
• Thường để tìm ra lỗi , ta phải xem xét thứ tự các hàm đã đc
kích hoạt ( theo vết) và hiển thị các giá trị các biến liên quan
• Nếu vẫn không phát hiện đc lỗi : dùng các BreakPoint hoạc
chạy từng bước – step by step
• Có nhiều công cụ gỡ rối mạnh và hiệu quả, tại sao ta vẫn mất
nhiều thời gian và trí lực để gỡ rối ?
• Nhiều khi các công cụ không thể giúp dễ dàng tìm lỗi, nếu đưa
ra 1 câu hỏi sai, trình gỡ rối sẽ cho 1 câu trả lời, nhưng ta có
thể không biết là nó đang bị sai
Sử dụng trình gỡ rối
• Dùng 1 trình gỡ rối để chạy từng bước là phương sách cuối cùng
• Nhiều khi vấn đề tưởng quá đơn giản nhưng lại không phát hiện được,
ví dụ các toán tử so sánh trong pascal va VB có độ ưu tiên ngang
nhau, nhưng với C ?
( == và != nhỏ hơn !)
Thứ tự các đối số của lời gọi hàm : ví dụ : strcpy(s1,s2)
•
•
int m[6]={1,2,3,4,5,6}, *p,*q;
p=m; q=p+2; *p++ =*q++; *p=*q; ???
Lỗi loại này khó tìm vì bản thân ý nghĩ của ta vạch ra 1 hướng suy
nghĩ sai lệch : coi điều không đúng là đúng
• Đôi khi lỗi là do nguyên nhân khách quan : Trình biên dịch, thư viện
hay hệ điều hành, hoặc lỗi phần cứng : 1994 lỗi xử lý dấu chấm
độngng trong bộ xử lý Pentium
Công cụ hỗ trợ gỡ rối
GDB: The GNU Project Debugger
• Gỡ rối được các chương trình viết bằng Ada, C,
C++, Objective-C, Pascal, v.v., chạy trên cùng
máy cài đặt GDB hay trên máy khác
• Hoạt động trên nền UNIX và Microsoft Windows
• Các chức năng hỗ trợ:
– Bắt đầu chương trình, xác định những yếu tố làm ảnh
hưởng đến hoạt động của chương trình
– Dừng chương trình với điều kiện biết trước
– Khi chương trình bị dừng, kiểm tra những gì đã xảy ra
– Thay đổi các lệnh trong chương trình để LTV có thể thử
nghiệm gỡ rối từng lỗi một
2.8. Tập trung vào các lệnh mới viết /
mới viết lại
• Kiểm tra sự thay đổi mới nhất
– Lỗi thường xảy ra ở những đoạn CT mới được bổ sung
– Nếu phiên bản cũ OK, phiên bản mới có lỗi => lỗi chắc
chắn nằm ở những đoạn CT mới
– Lưu ý, khi sửa đổi, nâng cấp : hãy giữ lại phiên bản cũ –
đơn giản là comment lại đoạn mã cũ
– Đặc biệt, với các hệ thống lớn, làm việc nhóm thì việc sử
dụng các hệ thống quản lý phiên bản mã nguồn và các
cơ chế lưu lại quá trình sửa đổi là vô cùng hữu ích (
source safe )
Tập trung vào các lệnh mới viết / mới
viết lại
• Các lỗi xuất hiện thất thường :
– Khó giải quyết
– Thường gán cho lỗi của máy tính, hệ điều hành …
– Thực ra là do thông tin của chính CT : không phải do thuật
toán, mà do thông tin bị thay đổi qua mỗi lần chạy
– Các biến đã đc khởi tạo hết chưa ?
– Lỗi cấp phát bộ nhớ ? Vd :
char *vd( char *s) {
char m[101];
strncpy(m,s,100)
return m;
}
– Giải phóng bộ nhớ động ?
– for (p=listp; p!=NULL; p=p->next) free(p) ; ???
Tập trung vào các lệnh mới viết / mới viết lại
• Phải gỡ rối ngay, không nên để sau
– Khó: Viết toàn bộ chương trình; kiểm tra toàn bộ
chương trình, gỡ rối toàn bộ chương trình
– Dễ hơn: Viết từng đoạn, kiểm tra từng đoạn, gỡ rối
• Nên giữ lại các phiên bản trước
từng đoạn; viết từng đoạn, kiểm tra từng đoạn, gỡ rối
từng đoạn;
– Khó: Thay đổi mã nguồn, đánh dấu các lỗi; cố gắng nhớ
xem đã thay đổi cái gì từ lần làm việc trước
– Dễ hơn: Backup mã nguồn, thay đổi mã nguồn, đánh
dấu các lỗi; so sánh phiên bản mới với phiên bản cũ để
xác định các điểm thay đổi
Giữ lại các phiên bản trước đó
• Cách 1: Sao chép bằng tay vào 1 thư mục
…
$ mkdir myproject
$ cd myproject
Create project files here.
$ cd ..
$ cp –r myproject myprojectDateTime
$ cd myproject
Continue creating project files here.
…
• Lặp lại mỗi lần có phiên bản mới
• Cách 2: dùng công cụ như RCS (Revision Control
System), CVS, v.v.
Tóm lại
• Gỡ rối là 1 nghệ thuật mà ta phải luyện tập
thường xuyên
• Nhưng đó là nghệ thuật mà ta không muốn
• Mã nguồn viết tốt có ít lỗi hơn và dễ tìm hơn
• Đầu tiên phải nghĩ đến nguồn gốc sinh ra lỗi
• Hãy suy nghĩ kỹ càng, có hệ thống để định vị khu
vực gây lỗi
• Không gì bằng học từ chính lỗi của mình – điều
này càng đúng đối với LTV
• Các trường hợp khác?
This is wrong. Why?
– Nearly full – Exactly full – Over full
Ví dụ • Rewrite yet again
s[i] = '\0';
for (i=0; ; i++) { int c = getchar(); if (c==EOF || c=='\n' || i==MAXLINE-1) { break; } else s[i] = c; }
There’s still a problem...
Where’s the ‘d’?
Input: Four score and seven years
Output: FourØ score anØ sevenØ yearsØ
Ambiguity in Specification
• Nếu dòng quá dài, xử lý thế nào?
– Giữ MAXLINE ký tự đầu, bỏ qua phần còn lại? – Giữ MAXLINE ký tự đầu + ‘\0’, bỏ qua phần còn lại? – Giữ MAXLINE ký tự đầu+’\0’, lưu phần còn lại cho lần gọi
sau của input function?
• Có thể phần đặc tả - specification không hề đề cập khi
MAXLINE bị quá – Có thể người ta không muốn dòng dài quá giới hạn trong
mọi trường hợp
– Đạo đức: kiểm tra đã phát hiện ra một vấn đề thiết kế,
thậm chí có thể là một vấn đề đặc điểm kỹ thuật !
• Quyết định phải làm gì
– Cắt những dòng quá dài? – Lưu phần còn lại để đọc như 1 dòng mới?
Kiểm tra điều kiện trước và sau
• Xác định những thuộc tính cần đi trước (điều kiện trước) và
sau (điều kiện sau) để mã nguồn được thi hành
• Ví dụ: các giá trị đầu vào phải thuộc 1 phạm vi cho trước
double avg( double a[], int n) {
int i; double sum=0.0;
for ( i = 0; i
sum+=a[i];
return sum/n;
• Nếu n=0 ?, nếu n<0 ?
}
• Có thể thay : return n <=0 ? 0.0: sum/n;
b. Statement Testing
• “Testing để thỏa mãn điều kiện rằng
mỗi lệnh trong 1 CT phải thực hiện ít
nhất 1 lần khi testing.”
Ví dụ
• Statement testing pseudocode:
Statement testing:
Phải chắc chắn các lệnh “if”
và 4 lệnh trong các nhánh
phải được thực hiện
if (condition1)
statement1;
else
statement2;
if (condition2)
statement3;
else
statement4;
…
– condition1 là đúng và condition2 là đúng
• Thực hiện statement1 và statement3
– condition1 là sai và condition2 là sai
• Thực hiện statement2 và statement4
• Đòi hỏi 2 tập dữ liệu. Ví dụ:
c. Path Testing
• Kiểm tra để đáp ứng các tiêu chuẩn đảm bảo
rằng mỗi đường dẫn logic xuyên suốt chương
trình được kiểm tra. Thường thì đường dẫn xuyên
suốt chương trình này được nhóm thành một tập
hữu hạn các lớp. Một đường dẫn từ mỗi lớp sau
đó được kiểm tra.
• Khó hơn nhiều so với statement testing
– Với các CT đơn giản, có thể liệt kê các nhánh đường dẫn
xuyên suốt code
– Ngược lại, bằng các đầu vào ngẫu nhiên tạo các đường
dẫn theo CT
Ví dụ
Path testing:
Cần đảm bảo tất cả các luồng
công việc đều được kiểm thử
if (condition1)
statement1;
else
statement2;
…
if (condition2)
statement3;
else
statement4;
…
• Đòi hỏi 4 tập dữ liệu:
– condition1 là true và condition2 là true
– condition1 là true và condition2 là false
– condition1 là false và condition2 là true
– condition1 là false và condition2 la false
• Chương trình thực tế => bùng nổ các tổ hợp!!!
Bài tập
1
F
else
A>0
T
3
2
B>0
T
F
(1) input(A,B)
if (A>0)
(2) Z = A;
(3) Z = 0;
if (B>0)
(4) Z = Z+B;
(5) output(Z)
4
5
What is the path condition for path <1,2,5>?
(A>0) Л (B0)
Bài tập
1
A>B
T
F
2
F
B<0
T
4
3
(1) input(A,B)
if (A>B)
(2) B = B*B;
if (B<0)
(3) Z = A;
else
(4) Z = B;
(5) output(Z)
5
What is the path condition for path <1,2,3,5>?
(A>B) Л (B<0)
Consider ANOTHER example…
1
A>B
T
F
F
2
T
B<0
T
4
3
5
What is the path condition for path <1,2,3,5>?
(A>B) Л (B<0) (B2<0) = FALSE
(1) input(A,B)
if (A>B)
(2) B = B*B;
if (B<0)
(3) Z = A;
else
(4) Z = B;
(5) output(Z)
d. Stress Testing
• “Tiến hành thử nghiệm để đánh giá một hệ thống
hay thành phần tại hoặc vượt quá các giới hạn
của các yêu cầu cụ thể của nó”
• Phải tạo:
– Một tập lớn đầu vào - Very large inputs
– Các đầu vào ngẫu nhiên - Random inputs (binary vs.
• Nên dùng máy tính để tạo đầu vào
ASCII)
Ví dụ: Stress Testing
• Example program:
Stress testing: Phải cung cấp
các đầu vào ngầu nhiên dạng
nhị phân hay ASCII
#include
int main(void) {
char c;
while ((c = getchar()) != EOF)
putchar(c);
return 0;
}
• Mục tiêu: Copy tất cả các ký tự từ stdin vào stdout; nhưng
lưu ý bug!!!
• Làm việc với tập dữ liệu ASCII chuẩn ( tự tạo)
• Máy tính tự tạo ngẫu nhiên tập dữ liệu dạng 255 (decimal),
hay 11111111 (binary), và EOF để dừng vòng lặp
Ví dụ: Stress Testing
Stress testing: Phải cung cấp
very large inputs
• Example program:
#include
int main(void) {
short charCount = 0;
while (getchar() != EOF)
charCount++;
printf("%hd\n", charCount);
return 0;
}
• Mục tiêu: Đếm và in số các kỹ tự trong stdin
• Làm việc với tập dữ liệu có kích thước phù hợp
• Sẽ có lỗi với tập dữ liệu do máy tạo chứa hơn
32767 characters
3. Internal Testing
• Internal testing: Thiết kế chương trình để nó tự
kiểm thử chính nó
• Internal testing techniques
conservation properties
(1) Kiểm tra các giá trị bất biến - Testing invariants
(2) Kiểm tra các thuộc tính cần được bảo lưu - Verifying
(3) Kiểm tra các giá trị trả về - Checking function return
values
(4) Thay đổi mã nguồn tạm thời - Changing code
temporarily
(5) Giữ lại phần mã kiểm tra - Leaving testing code intact
a. Testing Invariants
• Kiểm tra các điều kiện tiên nghiệm và điều kiện
hậu nghiệm
• 1 số khía cạnh của CTDL là bất biến, vì vậy, khi 1
hàm tác động lên CTDL, phải kiểm tra tính bất
biến của CTDL trước và trong khi tác động.
• Ví dụ: hàm thêm vào danh sách nối kép
– Duyệt danh sách nối kép
– Nếu node x trỏ đến node y, vậy node y có nhất định phải
• Ví dụ: hàm thêm nút vào cây nhị phân tìm kiếm
– Duyệt cây
– Sau khi thêm các nút còn được sắp đúng thứ tự không ?
trỏ đến node x hay không ?
b. Kiểm tra các đặc tính cần được bảo
lưu
• Khái quát hóa của testing invariants
• 1 hàm cần kiểm tra các cấu trúc dữ liệu bị tác
động tại các điểm đầu và cuối
• VD: hàm Str_concat()
• VD: Hàm chèn thêm PT vào danh sách -List
insertion function
– Tại điểm khởi đầu, tính độ dài ds
– Tại điểm cuối, Tính độ dài mới
– Độ dài mới = độ dài cũ + 1?
– Tại điểm đầu, tìm độ dài của 2 xâu đã cho; tính tổng
– Tại điểm cuối, tìm độ dài của xâu kết quả
– 2 độ dài có bằng nhau không ?
c. Kiểm tra các giá trị trả về
• Java/C++:
– Các phương thức phát hiện lỗi có thể bỏ qua 1 trường
hợp ngoại lệ đã được biết đến (“throw a checked
exception”)
– Việc gọi 1 phương thức cần phải kiểm soát được ngoại lệ
• C:
(hoặc lại bỏ qua ngoại lệ đó)
– Không có kỹ thuật kiểm soát các trường hợp ngoại lệ
– Hàm phát hiện ra lỗi thông qua giá trị trả về
– LTV thường quên kiểm tra giá trị trả về nên kiểm tra
giá trị trả về
Kiểm tra các giá trị trả về
Bad code
Good code
– scanf() trả về số giá trị đã đọc
int i;
scanf("%d", &i);
int i;
if (scanf("%d", &i) != 1)
/* Error */
– printf() có thể bị lỗi nếu ghi vào file hay đĩa đầy; trả
về số các ký tự (ko phải giá trị) đã ghi
Bad code??? Good code, or overkill???
int i = 100;
printf("%d", i);
int i = 100;
if (printf("%d", i) != 3)
/* Error */
d. Thay đổi mã nguồn tạm thời
• Thay đổi mã nguồn tạm thời để kiểm tra các điều kiện
biên và kiểm tra trực tiếp các giá trị
• Ví dụ: chương trình sắp xếp trên mảng:
– Dùng mảng với kích thước nhỏ
– CT có kiểm soát được lỗi tràn bộ nhớ hay không ?
• Nhớ áp dụng vào bài tập
• Ví dụ: dùng bảng băm
– Tạm thời cho hàm băm trả về kết quả là 1 hằng số
– Như vậy toàn bộ các giá trị cần băm sẽ có cùng 1 chỉ số,
tức là số lượng các giá trị cần băm có cùng chỉ số là rất lớn
– CT có kiểm soát được tình huống này hay không ?
Thay đổi mã nguồn tạm thời
• Viết 1 phiên bản hàm cấp phát bộ nhớ và phát
hiện ra lỗi sớm, để kiểm thử đoạn mã nguồn bị lỗi
thiếu bộ nhớ:
void *testmalloc( size_t n) {
static int count =0;
if (++count > 10)
return NULL;
else
return malloc(n);
}
e. Giữ lại phần mã kiểm tra
• Để nguyên phần mã kiểm tra quan trọng trong mã nguồn
• Có thể khoanh vùng chúng lại như là mã gỡ rối:
#ifndef NDEBUG … #endif
– Kích hoạt/ tắt các assert macro
– Kích hoạt/ tắt các đoạn mã gỡ rối của LTV
• Khi dịch thì dùng tùy chọn –DNDEBUG của gcc
– Việc viết các đoạn mã tự kiểm tra cho CT có thể làm giảm chi phí
bảo trì chương trình
– Mã nguồn sáng sủa cũng làm giảm chi phí bảo trì chương trình
– Nhưng: việc viết các đoạn mã tự kiểm tra khắp nơi sẽ làm
• Đề phòng xung đột:
giảm tính rõ ràng của chương trình
4. Các chiến lược kiểm thử tổng quát
• Kiểm thử dần dần -Testing incrementally
• So sánh các cách cài đặt khác nhau -Comparing
implementations
• Tự động kiểm thử – Automation
• Kiểm thử theo lỗi - Bug-driven testing
• Tiêm, gài lỗi - Fault injection
a. Testing Incrementally
• Test khi viết code
– Thêm tests khi tạo 1 lựa chọn mới - new cases
– Test phần đơn giản trước phần phức tạp
– Test units (tức là từng module riêng lẻ) trước khi testing
• Thực hiện regression testing – kiểm thử hồi quy
– Xử lý đc 1 lỗi thường tạo ra những lỗi mới trong 1 he
toàn hệ thống
thống lớn, vì vậy …
– Phải đảm bảo chắc chắn hệ thống không “thoái lui” kiểu
như chức năng trước kia đang làm việc giờ bị broken,
nên…
– Test mọi khả năng để so sanh phiên bản mới với phiên
bản cũ
Testing Incrementally (cont.)
• Tạo “giàn giáo” - scaffolds và “mẫu” -stubs để
test đoạn code mà ta quan tâm
Hàm gọi đến code
mà ta quan tâm
Đoạn code cần quan tâm
Hàm được gọi
bởi đoạn code cần
quan tâm
Hàm được gọi
bởi đoạn code cần
quan tâm
b. So sánh các cài đặt
• Hãy chắc chắn rằng các triển khai độc lập hành
xử như nhau
• Ví dụ: So sánh hành vi của chương trình mà bạn
dịch ( TB C++3.0 ) với GCC
• Ví dụ: So sánh hành vi của các hàm bạn tạo
trong str.h với các hàm trong thư viện string.h
• Trường hợp có thể tính 1 kết quả bẳng 2 cách
khác nhau (2 thuật toán giải quyết cùng 1 bài
toán): xây dựng 2 chương trình và kiểm tra kết
quả thực hiện
– Kết quả giống nhau: 2 chương trình cùng đúng
– Khác nhau: ít nhất 1 trong 2 chương trình bị sai
c. Tự động kiểm thử
• Kiểm thử thủ công rất nặng nhọc, tốn kém và nhàm
chán thậm chí không đủ độ tin cậy.
• Trong quá trình kiểm thử, phải
– Thực hiện lặp đi lặp lại các thao tác kiểm thử
– Dùng nhiều bộ dữ liệu nhập
– Nhiều lần so sánh dữ liệu đầu ra
• Tạo testing code để tránh các nhược điểm nói trên
– Viết 1 bộ kiểm thử để kiểm tra toàn bộ chương trình mỗi
khi có sự thay đổi, sau khi biên dịch thành công
– Cần biết cái gì được chờ đợi
• Tạo ra các đầu ra, sao cho dễ dàng nhận biết là đúng hay sai
– Tự động kiểm thử tốt hơn nhiều so với kiểm thử thủ công
Tự động hóa kiểm thử hồi quy
• Tuần tự so sánh kết quả thực hiện các phiên bản mới
với kết quả thực hiện những phiên bản cũ tương ứng.
• Mục đích : đảm bảo việc sửa lỗi sẽ không làm ảnh
hưởng những phần khác trừ khi chúng ta muốn
• 1 số hệ thống có công cụ trợ giúp kiểm thử tự động :
– Ngôn ngữ scripts : cho phép viết các đoạn script để test
tuần tự
– Unix : có các thao tác trên tệp tin như cmp và diff để so
sanh dữ liệu xuất, sort sắp xếp các phần tử, grep để kiểm
thử dữ liệu xuất, wc, sum va freq để tổng kết dữ liệu xuất
• Khi kiểm thử hồi quy, cần đảm bảo phiên bản cũ là
đúng, nếu sai thì rất khó xác định và kết quả sẽ
không chính xác
• Cần phải kiểm tra chính việc kiểm thử hồi quy 1 cách
định kỳ để đảm bảo nó vẫn hợp lệ
Tạo ra những kiểm thử độc lập
• Kiểm thử độc lập với các giá trị nhập và giá trị xuất mong đợi sẽ bổ
xung cho kiểm thử hồi quy
• VD: Dùng ngôn ngữ NewAwk thực hiện kiểm thử 1 CT ngắn, ghi dữ
liệu đẩu ra của CT vào 1 tệp, ghi kết quả đúng vào 1 tệp khác rồi so
sánh 2 tệp, nếu khác nhau thì thông báo lỗi
echo 3 5 | newawk ‘{i=1; print ($si)++; print $i ,i}’ > out1
echo ‘3 4 1’ > out2 #kết quả đúng
if ! Cmp –s out1 out2 # nếu kq so sánh khác nhau
then
echo ‘BAD: test failed’
Fi
• Mọt lỗi có thể cần nhiều thao tác kiểm thử hoặc phải kiểm tra toàn bộ
các lớp mới, hoặc có thể thêm vào những đoạn CT bảo vệ để có thể
bắt đc những lỗi trong CT
d. Bug-Driven Testing
• Tìm thấy 1 bug
Ngay lập tức tạo 1 test để bắt lỗi
• Đơn giản hóa việc kiểm thử hồi quy
e. Fault injection
• Chủ động (tạm thời) cài các bugs!!!
• Rồi quyết định nếu tìm thấy chúng
• Kiểm thử chính đoạn mã kiểm thử/ quy trình
kiểm thử
4. Ai kiểm thử cái gì
• Programmers: White-box testing
– Thuận lợi: Người triển khai nắm rõ mọi luồng dữ liệu
– Khó khăn: Bị ảnh hưởng bởi cách thiết kế CT hay viết
• Quality Assurance (QA) engineers: Black-box
testing
– Thuận lợi: Không có khái niệm về implementation
– Khó khăn: Không muốn test mọi logical paths
• Customers: Field testing
– Thuận lợi: Có các cách sử dụng CT bất ngờ; dễ gây lỗi
– Khó khăn: Không đủ trường hợp; khách hàng không
mã nguồn
thích tham gia vào quá trình test
Các kỹ thuật kiểm thử
• Black-Box: Testing chỉ dựa trên việc
phân tích các yêu cầu - requirements
(unit/component specification, user
documentation, v.v.). Còn được gọi là
functional testing.
• White-Box: Testing dựa trên việc
phân tích các logic bên trong - internal
logic (design, code, v.v.). (Nhưng kết
quả mong đợi vẫn đến từ
requirements.) Còn đc gọi là structural
testing.
Các mức độ kiểm thử
• Unit: testing các mẫu công việc nhỏ nhất của LTV để có thể
lập kế hoạch và theo dõi hợp lý (vd : function, procedure,
module, object class, ….)
• Component: testing 1 tập hợp các units tạo thành 1 thành
phần (vd : program, package, task, interacting object
classes, …)
• Product: testing các thành phần tạo thành 1 sản phẩm (
subsystem, application, …)
– Bắt đầu = functional (black-box) tests,
– Rồi thêm = structural (white-box) tests, và
– Tiến hành từ unit level đến system level với 1 hoặc một vài bước
tích hợp
• System: testing toàn bộ hệ thống
• Testing thường:
Tại sao không "test everything"?
20 x 4 x 3 x 10 x 2 x 100 = 480,000 tests
Chi phí cho 'exhaustive' testing:
Nếu 1 giây cho 1 test, 8000 phút, 133 giờ, 17.7 ngày
(chưa kể nhầm lẫn hoặc test đi test lại)
nếu 10 secs = 34 wks, 1 min = 4 yrs, 10 min = 40 yrs
Kiểm thử bao nhiêu thì đủ?
• Không bao giờ đủ !
• Khi bạn thực hiện những test mà bạn đã lên kế hoạch
• Khi khách hàng/người sử dụng thấy thỏa mãn
• Khi bạn đã chứng minh đc rằng hệ thống hoạt động
đúng, chính xác
• Khi bạn tin tưởng rằng HT hoạt động tốt
• Phụ thuộc vào các rủi ro có thể xảy ra với hệ thống
– Thời gian để test luôn có giới hạn
– Dùng RISK để xác định:
• Cái gì phải test trước
• Cái gì phải test nhiều
• Mỗi phần tử cần test kỹ đến mức nào?
• Tại một thời điểm nhất định, cái gì không cần test
IV. GỠ RỐI
1. Tổng quan về gỡ rối chương trình
2. Tìm kiếm và gỡ rối
Mở đầu
• Gỡ rối chương trình lớn là công việc không đơn
giản
• 1 LTV giỏi phải biết
• Debuggers
• Version control systems
– Nhiều chiến lược gỡ rối khác nhau
– Các công cụ hỗ trợ việc gỡ rối
1. Tổng quan về gỡ rối chương trình
• Gỡ rối là gì ?
– Khi chương trình bị lỗi, gỡ rối là các công việc cần làm để làm cho chương trình dịch
thông, chạy thông
– Thật không may, gỡ rối luôn là thao tác phải làm khi lập trình, thao tác này rất tốn
kém
Cách tốt nhất vẫn là phòng ngừa
•
– Khi bắt đầu gỡ rối chương trình, bạn đã biết là chương trình không chạy.
– Nếu bạn biết lý do tại sao chương trình không chạy, bạn có thể sửa được chương trình
cho nó chạy
– Nếu bạn hiểu chương trình của bạn, bạn sẽ có ít sai lầm và dễ dàng sửa chữa sai sót
hơn. Bí quyết là viết mã đơn giản, hiệu quả, chú thích hợp lý.
• Đối với mã nguồn, tiêu chí nào quan trọng hơn: rõ ràng hay chính xác ?
– Nếu mã nguồn rõ ràng, bạn có thể làm cho chương trình trở nên chính xác.
– Bạn có chắc là làm cho chương trình trở nên chính xác nếu nó không rõ ràng hay
không ?
• Nếu chương trình được thiết kế với cấu trúc tốt, được viết bằng phong cách lập
trình tốt và áp dụng các kỹ thuật viết chương trình hiệu quả, bẫy lỗi thì chi phí
cho việc gỡ rối sẽ được giảm thiểu.
2. Tìm kiếm và gỡ rối
• Khi có lỗi, ta thường đổ cho trình dịch, thư viện hay bất cứ nguyên
nhân khách quan nào khác …tuy nhiên, cuối cùng thì lỗi vẫn là lỗi của
CT, và trách nhiệm gỡ rối thuộc về LTV
Phải hiểu vấn đề xuất phát từ đâu thì mới giải quyết được:
•
– Lỗi xảy ra ở đâu? Hầu hết các lỗi thường đơn giản và dễ tìm. Hãy khảo sát các
đầu mối và cố gắng xác định được đoạn mã nguồn gây lỗi
– Lỗi xảy ra như thế nào? Khi đã có 1 số thông tin về lỗi và nơi xảy ra lỗi, hãy
suy nghĩ xem lỗi xảy ra như thế nào
– Đâu là nguyên nhân gây lỗi? Suy luận ngược trở lại trạng thái của CT để xác
định nguyên nhân gây ra lỗi
• Gỡ rối liên quan đến việc suy luận lùi, giỗng như phá án.
1 số vấn đề không thể xảy ra và chỉ có những thông tin xác thực mới
đáng tin cậy
Phải đi ngược từ kết quả để khám phá nguyên nhân,
Khi có lời giải thích đầy đủ, ta sẽ biết được vấn đề cần sửa và có thể
phát hiện ra 1 số vấn đề khác
2. Tìm kiếm và gỡ rối
Debugging Heuristic Áp dụng khi nào
(2) Nghĩ trước khi viết lại chương trình
(1) Hiểu các thông báo lỗi (error messages) Build-time (dịch)
(3) Tìm kiếm các lỗi (bug) hay xảy ra
(4) Divide and conquer Run-time (chạy)
(5) Viết thêm các đoạn mã kiểm tra để
chương trình tự kiểm tra nó
(6) Hiện thị kết quả
(7) Sử dụng debugger
(8) Tập trung vào các lệnh mới viết / mới viết
lại
2.1. Hiểu các thông báo lỗi
Gỡ rối khi dịch (build-time) CT dễ hơn gỡ rối khi
chạy CT nếu LTV hiểu được các thông báo lỗi
Một số thông báo lỗi đến từ preprocessor
Gõ sai tên file cần gọi
Thiếu dấu */
#include
int main(void)
/* Print "hello, world" to stdout and
return 0.
{
printf("hello, world\n");
return 0;
}
$ gcc217 hello.c -o hello
hello.c:1:20: stdioo.h: No such file or directory
hello.c:3:1: unterminated comment
hello.c:2: error: syntax error at end of input
Hiểu các thông báo lỗi
• Một số thông báo lỗi đến từ compiler
#include
int main(void)
/* Print "hello, world" to stdout and
return 0. */
{
printf("hello, world\n")
retun 0;
}
Hiểu các thông báo lỗi
• Một số thông báo lỗi đến từ linker
#include
int main(void)
/* Print "hello, world" to stdout and
return 0. */
{
prinf("hello, world\n")
return 0;
}
2.2. Nghĩ trước khi viết lại chương
trình
Việc thay đổi mã nguồn không hợp lý có thể gây ra
nhiều vấn đề hơn là để nguyên không thay đổi gì,
nên…
Phải nghĩ trước khi làm
Nghĩ trước khi viết lại chương trình
• Gỡ rối ngay khi gặp
– Khi phát hiện lỗi, hãy sửa ngay, đừng để sau mới sửa, vì có thể lỗi
không xuất hiện lại (do tình huống)
– Cân nhắc: việc sửa chữa này có ảnh hưởng tới các tình huống khác
hay không ?
• Quan sát lỗi từ góc độ khác
– Viết đoạn mã nguồn gây lỗi ra giấy
• Đừng chép hết cả đoạn không có nguy cơ gây lỗi, hoặc in toàn bộ
code ra giấy in => phá vỡ cây cấu trúc
– Vẽ hình minh họa các cấu trúc dữ liệu
• Nếu mà giải thuật làm thay đổi CTDL, vẽ lại hình trước khi viết lại
giải thuật
– Đọc trước khi gõ vào
• Đừng vội vàng, khi không rõ điều gì thực sự gây ra lỗi và sửa
không đúng chỗ sẽ có nguy cơ gây ra lỗi khác
Nghĩ trước khi viết lại chương trình
• Tạm dừng viết CT
– Khi gặp vấn đề, khó khăn, chậm tiến độ, lập tức thay đổi công việc =>
rút ra khỏi luồng quán tính sai lầm …
– Bỏ qua đoạn CT có lỗi
– Khi nào cảm thấy sẵn sàng thì chữa
• Giải thích logic của đoạn mã nguồn:
– Cho chính bạn
• Tạo điều kiện để suy nghĩ lại
– Cho ai khác có thể phản bác
• Extrem programming : làm việc theo cặp, pair programming,
người này LT, người kia kiểm tra, và ngược lại
– Cho cái gì đó không thể phản bác (cây, cốc trà đá, gấu bông, cún,…)
• Tạo điều kiện củng cố suy luận của mình
2.3. Tìm kiếm các lỗi hay xảy ra
• Xem lại các quy tắc lập trình
int i;
…
scanf("%d", i);
char c;
…
c = getchar();
switch (i) {
case 0:
…
/* missing break */
case 1:
…
break;
…
}
if (i = 5)
…
while (c = getchar() != EOF)
…
if (5 < i < 10)
…
if (i & j)
…
Chú ý: nếu đặt chế độ
cảnh báo (warnings) khi
dịch thì hầu hết các lỗi
kiểu này sẽ được phát hiện
Tìm kiếm các lỗi hay xảy ra
• Khi gặp vấn đề, hãy liên tưởng đến những trường
hợp tương tự đã gặp
– Vd1 :
int n; scanf(“%d”,n); ?
– Vd2 :
int n=1; double d=PI;
printf(“%d %f \n”,d,n); ??
• Không khởi tạo biến (với C) cũng sẽ gây ra những
lỗi khó lường.
Tìm kiếm các lỗi hay xảy ra
• Làm cho lỗi xuất hiện lại
– Cố gắng làm cho lỗi có thể xuất hiện lại khi cần
– Nếu không được, thì thử tìm nguyên nhân tại sao lại
không làm cho lỗi xuất hiện lại
Các phương pháp gỡ rối
• Tránh mắc cùng 1 lỗi 2 lần : Sau khi sửa 1 lỗi, hãy suy nghĩ
xem có lỗi tương tự ở nơi nào khác không. VD :
break;
case ‘o’ : /* tên tệp ouput*/
outname = argv[i]; break;
case ‘f’ : from = atoi(argv[i]); break;
case ‘t’ : to = atoi(argv[i]); break;
for (i=1;i
2.4. Divide and Conquer
• Thu hẹp phạm vi
• Tập trung vào dữ liệu gây lỗi
Divide and Conquer
– Thử chương trình với các tham số đầu vào từ đơn giản đến phức
tạp, từ nhỏ đến lớn để tìm lỗi
– Ví dụ: chương trình lỗi với file đầu vào filex
• Tạo ra phiên bản copy của filex , tên là filexcopy
• Xoá bớt nửa sau của filexcopy
• Chạy chương trình với tham số đầu vào là filexcopy
– Nếu chạy thông => nửa đầu của filex không gây lỗi, loại bỏ nửa này,
tìm lỗi trong nửa sau của filex
– Nếu không chạy => nửa đầu của filex gây lỗi, loại bỏ nửa sau, tìm lỗi
trong nửa đầu của filex
• Lặp cho đến khi không còn dòng nào trong filex có thể bị loại bỏ
– Cách khác: bắt đầu với 1 ít dòng trong filex, thêm dần các dòng
vào cho đến khi thấy lỗi
• Khử đầu vào
Divide and Conquer
• Khử mã nguồn
– Thử chương trình với các đoạn mã nguồn từ ngắn đến
dài để tìm lỗi
– Example: đoạn chương trình có sử dụng đến các phần
• Viết riêng từng lời gọi hàm trong đoạn chương trình bị lỗi
(test client)
– Hoặc viết thêm phần kiểm tra gọi hàm vào phần CTC được gọi
• Chạy thử test client
• Không chạy => lỗi liên quan đến việc gọi/ thực hiện CTC
vừa thử
• Chạy thông => lỗi nằm trong phần còn lại, tiếp tục thử gọi
các hàm khác
CTC khác không chạy
2.5. Viết thêm các đoạn mã kiểm tra để
chương trình tự kiểm tra nó
• Dùng internal test để khử lỗi trong CT và giảm
nhẹ công việc tìm kiếm lỗi
– Chỉ cần viết thêm 1 hàm để kiểm tra, gắn vào trước và
sau đoạn có nguy cơ, comment lại sau khi đã xử lý xong
lỗi
đã học
• Kiểm tra các giá trị không thay đổi
• Kiểm tra các đặc tính cần bảo lưu
• Kiểm tra giá trị trả về của hàm
• Thay đổi mã nguồn tạm thời
• Kiểm tra mà không làm thay đổi mã nguồn
• Dùng assertion để nhận dạng các lỗi có trong CT
– Các kỹ thuật viết thêm mã tự kiểm tra cho chương trình
Assertions
• Assertion: 1 macro hay 1 CT con dùng trong quá trình
phát triển ứng dụng, cho phép CT tự kiểm tra khi
chạy.
• Return true >> OK, false >> có 1 lỗi gì đó trong CT.
• Ghi lại những giả thiết được đưa ra trong code
• Loại bỏ những điều kiện không mong đợi
• VD :
– Nếu hệ thống cho rằng file dữ liệu về khách hàng không
bao giờ vượt quá 50 000 bản ghi, CT có thể chứa 1
assertion rằng số bản ghi là <= 50 000.
– Khi mà số bản ghi <= 50,000, assertion sẽ không có phản
ứng gì.
– Nếu đếm đc hơn 50 000 bản ghi, nó sẽ lớn tiếng “khẳng
định” rằng có 1 lỗi trong CT
Assertions
• Assertions có thể được dùng để kiểm tra các giả thiết
như :
– Các tham số đầu vào nằm trong phạm vi mong đợi (tương
tự với các tham số đầu ra)
– File hay stream đang được mở (hay đóng) khi 1 CTC bắt
đầu thực hiện (hay kết thúc)
– 1 file hay stream đang ở bản ghi đầu tiên (hay cuối cùng)
khi 1 CTC bắt đầu ( hay kết thúc) thực hiện
– 1 file hay stream được mở để đọc, để ghi, hay cả đọc và ghi
– Giá trị của 1 tham số đầu vào là không thay đổi bởi 1 CTC
– 1 pointer là non-NULL
– 1 mảng đc truyền vào CTC có thể chứa ít nhất X phần tử
– 1 bảng đã đc khởi tạo để chứa các giá trị thực
– 1 danh sách là rỗng (hay đầy) lkhi 1 CTC bắt đầu (hay kết
thúc) thực hiện
Assertions
• End users không cần thấy
các thông báo của assertion ;
• Assertions chủ yếu đc dùng
trong quá trình phát triển
hay bảo dưỡng ứng dụng.
• Dịch thành code khi phát
triển, loại bỏ khỏi code trong
sản phẩm để nâng cao hiệu
năng của chương trình
• Rất nhiều NNLT hỗ trợ
assertions : C++, Java và
Visual Basic.
VD:
#define ASSERT(condition, message)
{
if ( !(condition) ) {
fprintf(
stderr,
"Assertion %s failed: %s\n",
condition,
message);
exit( EXIT_FAILURE );
}
}
• Kể cả khi NNLT không hỗ trợ,
thì cũng có thể dễ dàng xây
dựng
Dùng assertions như thế nào ?
• Bẫy lỗi cho những tình huống lường trước (sự
kiện ta chờ đợi sẽ xảy ra);
– Error-handling : checks for bad input data Hướng tới
• Dùng assertions cho các tình huống không lường
trước (sự kiện không mong đợi xảy ra hoặc không
bao giờ xảy ra)
– Assertions : check for bugs in the code hướng đến
việc xử lý lỗi
• Tránh đưa code xử lý vào trong assertions
– Điều gì xảy ra khi ta turn off the assertions ?
việc hiệu chỉnh chương trình, tạo ra phiên bản mới của
chương trình
Dùng assertions như thế nào ?
• Các chương trình lớn:
– trước tiên xác nhận lỗi (dùng assertion),
– sau đó bẫy lỗi (dùng error-handling)
• Nguyên nhân gây lỗi đã được xác định:
– hoặc dùng assertion, hoặc dùng error-handling,
– không dùng cả 2 cùng lúc
• Các chương trình cực lớn, nhiều người cùng phát triển
trong thời gian 5-10 năm, hoặc hơn nữa ?
– Cả assertions và error handling code có thể đc dùng cho
cùng 1 lỗi.
– Ví dụ trong source code cho Microsoft Word, những điều
kiện luôn trả về true thì đc dùng assertion, nhưng đồng thời
cũng đc xử lý.
– Assertions rất có lợi vì nó giúp loại bỏ rất nhiều lỗi trong
quá trình phát triển hệ thống
Kiểm tra các giá trị không thay đổi
• Có thể sử dụng assert để kiểm tra các giá trị không thay đổi
#ifndef NDEBUG
int isValid(MyType object) {
…
Test invariants here.
Return 1 (TRUE) if object passes
all tests, and 0 (FALSE) otherwise.
…
}
#endif
void myFunction(MyType object) {
assert(isValid(object));
…
Manipulate object here.
…
assert(isValid(object));
}
2.6. Hiện thị kết quả đầu ra
•
In giá trị của các biến tại các
điểm có khả năng gây lỗi để
định vị khu vực gây lỗi, hoặc
• Xác định tiến trình thực hiện :
“đến đây 1”
• Poor:
stdout is buffered;
CT có thể có lỗi
trước khi hiện ra
output
printf("%d", keyvariable);
• Maybe better:
printf("%d\n", keyvariable);
• Better:
In '\n' sẽ xóa bộ nhớ
đệm stdout , nhưng
sẽ không xóa khi in
ra file
Gọi fflush() để làm
sạch buffer 1 cách
tường minh
printf("%d", keyvariable);
fflush(stdout);
Hiện thị kết quả đầu ra
• Tạo log file
• Lưu vết
– Giúp ghi nhớ đc các vấn đề đã xảy ra,
và giải quyết các vđề tương tự sau
này, cũng như khi chuyển giao CT
cho người khác..
• Maybe even better:
In debugging
output ra stderr;
debugging output
có thể tách biệt
với đầu ra thông
thường bằng cách
in ấn của CT
fprintf(stderr, "%d", keyvariable);
• Maybe better still:
Ghi ra 1 a log file
Ngoài ra: stderr
không dùng buffer
FILE *fp = fopen("logfile", "w");
…
fprintf(fp, "%d", keyvariable);
fflush(fp);
2.7. Sử dụng trình gỡ rối (debugger)
IDE : kết hợp soạn thảo, biên dịch, gỡ rối …
•
• Các trình gỡ rối với giao diện đồ họa cho phép chạy chương
trình từng bước qua từng lệnh hoặc từng hàm, dừng ở những
dòng lệnh đặc biệt hay khi xuất hiện những đk đặc biệt, bên
canh đó có các công cụ cho phép định dạng và hiển thị giá trị
các biến, biểu thức
• Trình gỡ rối có thể được kích hoạt trực tiếp khi có lỗi hoặc gắn
vào chương trình đang chạy.
• Thường để tìm ra lỗi , ta phải xem xét thứ tự các hàm đã đc
kích hoạt ( theo vết) và hiển thị các giá trị các biến liên quan
• Nếu vẫn không phát hiện đc lỗi : dùng các BreakPoint hoạc
chạy từng bước – step by step
• Có nhiều công cụ gỡ rối mạnh và hiệu quả, tại sao ta vẫn mất
nhiều thời gian và trí lực để gỡ rối ?
• Nhiều khi các công cụ không thể giúp dễ dàng tìm lỗi, nếu đưa
ra 1 câu hỏi sai, trình gỡ rối sẽ cho 1 câu trả lời, nhưng ta có
thể không biết là nó đang bị sai
Sử dụng trình gỡ rối
• Dùng 1 trình gỡ rối để chạy từng bước là phương sách cuối cùng
• Nhiều khi vấn đề tưởng quá đơn giản nhưng lại không phát hiện được,
ví dụ các toán tử so sánh trong pascal va VB có độ ưu tiên ngang
nhau, nhưng với C ?
( == và != nhỏ hơn !)
Thứ tự các đối số của lời gọi hàm : ví dụ : strcpy(s1,s2)
•
•
int m[6]={1,2,3,4,5,6}, *p,*q;
p=m; q=p+2; *p++ =*q++; *p=*q; ???
Lỗi loại này khó tìm vì bản thân ý nghĩ của ta vạch ra 1 hướng suy
nghĩ sai lệch : coi điều không đúng là đúng
• Đôi khi lỗi là do nguyên nhân khách quan : Trình biên dịch, thư viện
hay hệ điều hành, hoặc lỗi phần cứng : 1994 lỗi xử lý dấu chấm
độngng trong bộ xử lý Pentium
Công cụ hỗ trợ gỡ rối
GDB: The GNU Project Debugger
• Gỡ rối được các chương trình viết bằng Ada, C,
C++, Objective-C, Pascal, v.v., chạy trên cùng
máy cài đặt GDB hay trên máy khác
• Hoạt động trên nền UNIX và Microsoft Windows
• Các chức năng hỗ trợ:
– Bắt đầu chương trình, xác định những yếu tố làm ảnh
hưởng đến hoạt động của chương trình
– Dừng chương trình với điều kiện biết trước
– Khi chương trình bị dừng, kiểm tra những gì đã xảy ra
– Thay đổi các lệnh trong chương trình để LTV có thể thử
nghiệm gỡ rối từng lỗi một
2.8. Tập trung vào các lệnh mới viết /
mới viết lại
• Kiểm tra sự thay đổi mới nhất
– Lỗi thường xảy ra ở những đoạn CT mới được bổ sung
– Nếu phiên bản cũ OK, phiên bản mới có lỗi => lỗi chắc
chắn nằm ở những đoạn CT mới
– Lưu ý, khi sửa đổi, nâng cấp : hãy giữ lại phiên bản cũ –
đơn giản là comment lại đoạn mã cũ
– Đặc biệt, với các hệ thống lớn, làm việc nhóm thì việc sử
dụng các hệ thống quản lý phiên bản mã nguồn và các
cơ chế lưu lại quá trình sửa đổi là vô cùng hữu ích (
source safe )
Tập trung vào các lệnh mới viết / mới
viết lại
• Các lỗi xuất hiện thất thường :
– Khó giải quyết
– Thường gán cho lỗi của máy tính, hệ điều hành …
– Thực ra là do thông tin của chính CT : không phải do thuật
toán, mà do thông tin bị thay đổi qua mỗi lần chạy
– Các biến đã đc khởi tạo hết chưa ?
– Lỗi cấp phát bộ nhớ ? Vd :
char *vd( char *s) {
char m[101];
strncpy(m,s,100)
return m;
}
– Giải phóng bộ nhớ động ?
– for (p=listp; p!=NULL; p=p->next) free(p) ; ???
Tập trung vào các lệnh mới viết / mới viết lại
• Phải gỡ rối ngay, không nên để sau
– Khó: Viết toàn bộ chương trình; kiểm tra toàn bộ
chương trình, gỡ rối toàn bộ chương trình
– Dễ hơn: Viết từng đoạn, kiểm tra từng đoạn, gỡ rối
• Nên giữ lại các phiên bản trước
từng đoạn; viết từng đoạn, kiểm tra từng đoạn, gỡ rối
từng đoạn;
– Khó: Thay đổi mã nguồn, đánh dấu các lỗi; cố gắng nhớ
xem đã thay đổi cái gì từ lần làm việc trước
– Dễ hơn: Backup mã nguồn, thay đổi mã nguồn, đánh
dấu các lỗi; so sánh phiên bản mới với phiên bản cũ để
xác định các điểm thay đổi
Giữ lại các phiên bản trước đó
• Cách 1: Sao chép bằng tay vào 1 thư mục
…
$ mkdir myproject
$ cd myproject
Create project files here.
$ cd ..
$ cp –r myproject myprojectDateTime
$ cd myproject
Continue creating project files here.
…
• Lặp lại mỗi lần có phiên bản mới
• Cách 2: dùng công cụ như RCS (Revision Control
System), CVS, v.v.
Tóm lại
• Gỡ rối là 1 nghệ thuật mà ta phải luyện tập
thường xuyên
• Nhưng đó là nghệ thuật mà ta không muốn
• Mã nguồn viết tốt có ít lỗi hơn và dễ tìm hơn
• Đầu tiên phải nghĩ đến nguồn gốc sinh ra lỗi
• Hãy suy nghĩ kỹ càng, có hệ thống để định vị khu
vực gây lỗi
• Không gì bằng học từ chính lỗi của mình – điều
này càng đúng đối với LTV
sum+=a[i];
return sum/n;
• Nếu n=0 ?, nếu n<0 ?
}
• Có thể thay : return n <=0 ? 0.0: sum/n;
b. Statement Testing
• “Testing để thỏa mãn điều kiện rằng mỗi lệnh trong 1 CT phải thực hiện ít nhất 1 lần khi testing.”
Ví dụ
• Statement testing pseudocode:
Statement testing:
Phải chắc chắn các lệnh “if” và 4 lệnh trong các nhánh phải được thực hiện
if (condition1) statement1; else statement2; if (condition2) statement3; else statement4; …
– condition1 là đúng và condition2 là đúng • Thực hiện statement1 và statement3 – condition1 là sai và condition2 là sai • Thực hiện statement2 và statement4
• Đòi hỏi 2 tập dữ liệu. Ví dụ:
c. Path Testing
• Kiểm tra để đáp ứng các tiêu chuẩn đảm bảo rằng mỗi đường dẫn logic xuyên suốt chương trình được kiểm tra. Thường thì đường dẫn xuyên suốt chương trình này được nhóm thành một tập hữu hạn các lớp. Một đường dẫn từ mỗi lớp sau đó được kiểm tra.
• Khó hơn nhiều so với statement testing
– Với các CT đơn giản, có thể liệt kê các nhánh đường dẫn
xuyên suốt code
– Ngược lại, bằng các đầu vào ngẫu nhiên tạo các đường
dẫn theo CT
Ví dụ
Path testing:
Cần đảm bảo tất cả các luồng công việc đều được kiểm thử
if (condition1) statement1; else statement2; … if (condition2) statement3; else statement4; …
• Đòi hỏi 4 tập dữ liệu:
– condition1 là true và condition2 là true – condition1 là true và condition2 là false – condition1 là false và condition2 là true – condition1 là false và condition2 la false
• Chương trình thực tế => bùng nổ các tổ hợp!!!
Bài tập
1
F
else
A>0 T
3
2 B>0 T
F
(1) input(A,B) if (A>0) (2) Z = A; (3) Z = 0; if (B>0) (4) Z = Z+B; (5) output(Z)
4
5
What is the path condition for path <1,2,5>?
(A>0) Л (B0)
Bài tập
1
A>B T
F
2
F
B<0 T
4
3
(1) input(A,B) if (A>B) (2) B = B*B; if (B<0) (3) Z = A; else (4) Z = B; (5) output(Z)
5
What is the path condition for path <1,2,3,5>?
(A>B) Л (B<0)
Consider ANOTHER example…
1
A>B T
F
F
2 T B<0 T
4
3
5
What is the path condition for path <1,2,3,5>?
(A>B) Л (B<0) (B2<0) = FALSE
(1) input(A,B) if (A>B) (2) B = B*B; if (B<0) (3) Z = A; else (4) Z = B; (5) output(Z)
d. Stress Testing
• “Tiến hành thử nghiệm để đánh giá một hệ thống hay thành phần tại hoặc vượt quá các giới hạn của các yêu cầu cụ thể của nó”
• Phải tạo:
– Một tập lớn đầu vào - Very large inputs – Các đầu vào ngẫu nhiên - Random inputs (binary vs.
• Nên dùng máy tính để tạo đầu vào
ASCII)
Ví dụ: Stress Testing
• Example program:
Stress testing: Phải cung cấp các đầu vào ngầu nhiên dạng nhị phân hay ASCII
#include
• Mục tiêu: Copy tất cả các ký tự từ stdin vào stdout; nhưng
lưu ý bug!!!
• Làm việc với tập dữ liệu ASCII chuẩn ( tự tạo) • Máy tính tự tạo ngẫu nhiên tập dữ liệu dạng 255 (decimal),
hay 11111111 (binary), và EOF để dừng vòng lặp
Ví dụ: Stress Testing
Stress testing: Phải cung cấp very large inputs
• Example program:
#include
int main(void) {
short charCount = 0;
while (getchar() != EOF)
charCount++;
printf("%hd\n", charCount);
return 0;
}
• Mục tiêu: Đếm và in số các kỹ tự trong stdin • Làm việc với tập dữ liệu có kích thước phù hợp • Sẽ có lỗi với tập dữ liệu do máy tạo chứa hơn
32767 characters
3. Internal Testing
• Internal testing: Thiết kế chương trình để nó tự
kiểm thử chính nó
• Internal testing techniques
conservation properties
(1) Kiểm tra các giá trị bất biến - Testing invariants (2) Kiểm tra các thuộc tính cần được bảo lưu - Verifying
(3) Kiểm tra các giá trị trả về - Checking function return
values
(4) Thay đổi mã nguồn tạm thời - Changing code
temporarily
(5) Giữ lại phần mã kiểm tra - Leaving testing code intact
a. Testing Invariants
• Kiểm tra các điều kiện tiên nghiệm và điều kiện
hậu nghiệm
• 1 số khía cạnh của CTDL là bất biến, vì vậy, khi 1 hàm tác động lên CTDL, phải kiểm tra tính bất biến của CTDL trước và trong khi tác động.
• Ví dụ: hàm thêm vào danh sách nối kép
– Duyệt danh sách nối kép – Nếu node x trỏ đến node y, vậy node y có nhất định phải
• Ví dụ: hàm thêm nút vào cây nhị phân tìm kiếm
– Duyệt cây – Sau khi thêm các nút còn được sắp đúng thứ tự không ?
trỏ đến node x hay không ?
b. Kiểm tra các đặc tính cần được bảo lưu
• Khái quát hóa của testing invariants • 1 hàm cần kiểm tra các cấu trúc dữ liệu bị tác
động tại các điểm đầu và cuối
• VD: hàm Str_concat()
• VD: Hàm chèn thêm PT vào danh sách -List
insertion function – Tại điểm khởi đầu, tính độ dài ds – Tại điểm cuối, Tính độ dài mới – Độ dài mới = độ dài cũ + 1?
– Tại điểm đầu, tìm độ dài của 2 xâu đã cho; tính tổng – Tại điểm cuối, tìm độ dài của xâu kết quả – 2 độ dài có bằng nhau không ?
c. Kiểm tra các giá trị trả về
• Java/C++:
– Các phương thức phát hiện lỗi có thể bỏ qua 1 trường
hợp ngoại lệ đã được biết đến (“throw a checked exception”)
– Việc gọi 1 phương thức cần phải kiểm soát được ngoại lệ
• C:
(hoặc lại bỏ qua ngoại lệ đó)
– Không có kỹ thuật kiểm soát các trường hợp ngoại lệ – Hàm phát hiện ra lỗi thông qua giá trị trả về – LTV thường quên kiểm tra giá trị trả về nên kiểm tra
giá trị trả về
Kiểm tra các giá trị trả về
Bad code
Good code
– scanf() trả về số giá trị đã đọc
int i; scanf("%d", &i);
int i; if (scanf("%d", &i) != 1) /* Error */
– printf() có thể bị lỗi nếu ghi vào file hay đĩa đầy; trả
về số các ký tự (ko phải giá trị) đã ghi
Bad code??? Good code, or overkill???
int i = 100; printf("%d", i);
int i = 100; if (printf("%d", i) != 3) /* Error */
d. Thay đổi mã nguồn tạm thời
• Thay đổi mã nguồn tạm thời để kiểm tra các điều kiện
biên và kiểm tra trực tiếp các giá trị
• Ví dụ: chương trình sắp xếp trên mảng:
– Dùng mảng với kích thước nhỏ – CT có kiểm soát được lỗi tràn bộ nhớ hay không ?
• Nhớ áp dụng vào bài tập
• Ví dụ: dùng bảng băm
– Tạm thời cho hàm băm trả về kết quả là 1 hằng số – Như vậy toàn bộ các giá trị cần băm sẽ có cùng 1 chỉ số,
tức là số lượng các giá trị cần băm có cùng chỉ số là rất lớn
– CT có kiểm soát được tình huống này hay không ?
Thay đổi mã nguồn tạm thời
• Viết 1 phiên bản hàm cấp phát bộ nhớ và phát
hiện ra lỗi sớm, để kiểm thử đoạn mã nguồn bị lỗi thiếu bộ nhớ:
void *testmalloc( size_t n) {
static int count =0;
if (++count > 10)
return NULL;
else
return malloc(n);
}
e. Giữ lại phần mã kiểm tra
• Để nguyên phần mã kiểm tra quan trọng trong mã nguồn • Có thể khoanh vùng chúng lại như là mã gỡ rối: #ifndef NDEBUG … #endif
– Kích hoạt/ tắt các assert macro – Kích hoạt/ tắt các đoạn mã gỡ rối của LTV
• Khi dịch thì dùng tùy chọn –DNDEBUG của gcc
– Việc viết các đoạn mã tự kiểm tra cho CT có thể làm giảm chi phí
bảo trì chương trình
– Mã nguồn sáng sủa cũng làm giảm chi phí bảo trì chương trình – Nhưng: việc viết các đoạn mã tự kiểm tra khắp nơi sẽ làm
• Đề phòng xung đột:
giảm tính rõ ràng của chương trình
4. Các chiến lược kiểm thử tổng quát
• Kiểm thử dần dần -Testing incrementally • So sánh các cách cài đặt khác nhau -Comparing
implementations
• Tự động kiểm thử – Automation • Kiểm thử theo lỗi - Bug-driven testing • Tiêm, gài lỗi - Fault injection
a. Testing Incrementally
• Test khi viết code
– Thêm tests khi tạo 1 lựa chọn mới - new cases – Test phần đơn giản trước phần phức tạp – Test units (tức là từng module riêng lẻ) trước khi testing
• Thực hiện regression testing – kiểm thử hồi quy – Xử lý đc 1 lỗi thường tạo ra những lỗi mới trong 1 he
toàn hệ thống
thống lớn, vì vậy …
– Phải đảm bảo chắc chắn hệ thống không “thoái lui” kiểu như chức năng trước kia đang làm việc giờ bị broken, nên…
– Test mọi khả năng để so sanh phiên bản mới với phiên
bản cũ
Testing Incrementally (cont.)
• Tạo “giàn giáo” - scaffolds và “mẫu” -stubs để
test đoạn code mà ta quan tâm
Hàm gọi đến code mà ta quan tâm
Đoạn code cần quan tâm
Hàm được gọi bởi đoạn code cần quan tâm
Hàm được gọi bởi đoạn code cần quan tâm
b. So sánh các cài đặt
• Hãy chắc chắn rằng các triển khai độc lập hành
xử như nhau
• Ví dụ: So sánh hành vi của chương trình mà bạn
dịch ( TB C++3.0 ) với GCC
• Ví dụ: So sánh hành vi của các hàm bạn tạo
trong str.h với các hàm trong thư viện string.h
• Trường hợp có thể tính 1 kết quả bẳng 2 cách khác nhau (2 thuật toán giải quyết cùng 1 bài toán): xây dựng 2 chương trình và kiểm tra kết quả thực hiện – Kết quả giống nhau: 2 chương trình cùng đúng – Khác nhau: ít nhất 1 trong 2 chương trình bị sai
c. Tự động kiểm thử
• Kiểm thử thủ công rất nặng nhọc, tốn kém và nhàm
chán thậm chí không đủ độ tin cậy.
• Trong quá trình kiểm thử, phải
– Thực hiện lặp đi lặp lại các thao tác kiểm thử – Dùng nhiều bộ dữ liệu nhập – Nhiều lần so sánh dữ liệu đầu ra
• Tạo testing code để tránh các nhược điểm nói trên
– Viết 1 bộ kiểm thử để kiểm tra toàn bộ chương trình mỗi
khi có sự thay đổi, sau khi biên dịch thành công
– Cần biết cái gì được chờ đợi
• Tạo ra các đầu ra, sao cho dễ dàng nhận biết là đúng hay sai – Tự động kiểm thử tốt hơn nhiều so với kiểm thử thủ công
Tự động hóa kiểm thử hồi quy
• Tuần tự so sánh kết quả thực hiện các phiên bản mới với kết quả thực hiện những phiên bản cũ tương ứng.
• Mục đích : đảm bảo việc sửa lỗi sẽ không làm ảnh hưởng những phần khác trừ khi chúng ta muốn
• 1 số hệ thống có công cụ trợ giúp kiểm thử tự động : – Ngôn ngữ scripts : cho phép viết các đoạn script để test
tuần tự
– Unix : có các thao tác trên tệp tin như cmp và diff để so
sanh dữ liệu xuất, sort sắp xếp các phần tử, grep để kiểm thử dữ liệu xuất, wc, sum va freq để tổng kết dữ liệu xuất
• Khi kiểm thử hồi quy, cần đảm bảo phiên bản cũ là đúng, nếu sai thì rất khó xác định và kết quả sẽ không chính xác
• Cần phải kiểm tra chính việc kiểm thử hồi quy 1 cách
định kỳ để đảm bảo nó vẫn hợp lệ
Tạo ra những kiểm thử độc lập
• Kiểm thử độc lập với các giá trị nhập và giá trị xuất mong đợi sẽ bổ
xung cho kiểm thử hồi quy
• VD: Dùng ngôn ngữ NewAwk thực hiện kiểm thử 1 CT ngắn, ghi dữ
liệu đẩu ra của CT vào 1 tệp, ghi kết quả đúng vào 1 tệp khác rồi so sánh 2 tệp, nếu khác nhau thì thông báo lỗi
echo 3 5 | newawk ‘{i=1; print ($si)++; print $i ,i}’ > out1 echo ‘3 4 1’ > out2 #kết quả đúng if ! Cmp –s out1 out2 # nếu kq so sánh khác nhau then echo ‘BAD: test failed’ Fi • Mọt lỗi có thể cần nhiều thao tác kiểm thử hoặc phải kiểm tra toàn bộ các lớp mới, hoặc có thể thêm vào những đoạn CT bảo vệ để có thể bắt đc những lỗi trong CT
d. Bug-Driven Testing
• Tìm thấy 1 bug Ngay lập tức tạo 1 test để bắt lỗi • Đơn giản hóa việc kiểm thử hồi quy
e. Fault injection
• Chủ động (tạm thời) cài các bugs!!! • Rồi quyết định nếu tìm thấy chúng • Kiểm thử chính đoạn mã kiểm thử/ quy trình
kiểm thử
4. Ai kiểm thử cái gì
• Programmers: White-box testing
– Thuận lợi: Người triển khai nắm rõ mọi luồng dữ liệu – Khó khăn: Bị ảnh hưởng bởi cách thiết kế CT hay viết
• Quality Assurance (QA) engineers: Black-box
testing – Thuận lợi: Không có khái niệm về implementation – Khó khăn: Không muốn test mọi logical paths
• Customers: Field testing
– Thuận lợi: Có các cách sử dụng CT bất ngờ; dễ gây lỗi – Khó khăn: Không đủ trường hợp; khách hàng không
mã nguồn
thích tham gia vào quá trình test
Các kỹ thuật kiểm thử
• Black-Box: Testing chỉ dựa trên việc phân tích các yêu cầu - requirements (unit/component specification, user documentation, v.v.). Còn được gọi là functional testing.
• White-Box: Testing dựa trên việc
phân tích các logic bên trong - internal logic (design, code, v.v.). (Nhưng kết quả mong đợi vẫn đến từ requirements.) Còn đc gọi là structural testing.
Các mức độ kiểm thử
• Unit: testing các mẫu công việc nhỏ nhất của LTV để có thể lập kế hoạch và theo dõi hợp lý (vd : function, procedure, module, object class, ….)
• Component: testing 1 tập hợp các units tạo thành 1 thành
phần (vd : program, package, task, interacting object classes, …)
• Product: testing các thành phần tạo thành 1 sản phẩm (
subsystem, application, …)
– Bắt đầu = functional (black-box) tests, – Rồi thêm = structural (white-box) tests, và – Tiến hành từ unit level đến system level với 1 hoặc một vài bước
tích hợp
• System: testing toàn bộ hệ thống • Testing thường:
Tại sao không "test everything"?
20 x 4 x 3 x 10 x 2 x 100 = 480,000 tests
Chi phí cho 'exhaustive' testing: Nếu 1 giây cho 1 test, 8000 phút, 133 giờ, 17.7 ngày
(chưa kể nhầm lẫn hoặc test đi test lại) nếu 10 secs = 34 wks, 1 min = 4 yrs, 10 min = 40 yrs
Kiểm thử bao nhiêu thì đủ?
• Không bao giờ đủ ! • Khi bạn thực hiện những test mà bạn đã lên kế hoạch • Khi khách hàng/người sử dụng thấy thỏa mãn • Khi bạn đã chứng minh đc rằng hệ thống hoạt động
đúng, chính xác
• Khi bạn tin tưởng rằng HT hoạt động tốt • Phụ thuộc vào các rủi ro có thể xảy ra với hệ thống
– Thời gian để test luôn có giới hạn – Dùng RISK để xác định: • Cái gì phải test trước • Cái gì phải test nhiều • Mỗi phần tử cần test kỹ đến mức nào? • Tại một thời điểm nhất định, cái gì không cần test
IV. GỠ RỐI
1. Tổng quan về gỡ rối chương trình 2. Tìm kiếm và gỡ rối
Mở đầu
• Gỡ rối chương trình lớn là công việc không đơn
giản
• 1 LTV giỏi phải biết
• Debuggers • Version control systems
– Nhiều chiến lược gỡ rối khác nhau – Các công cụ hỗ trợ việc gỡ rối
1. Tổng quan về gỡ rối chương trình
• Gỡ rối là gì ?
– Khi chương trình bị lỗi, gỡ rối là các công việc cần làm để làm cho chương trình dịch
thông, chạy thông
– Thật không may, gỡ rối luôn là thao tác phải làm khi lập trình, thao tác này rất tốn
kém
Cách tốt nhất vẫn là phòng ngừa
•
– Khi bắt đầu gỡ rối chương trình, bạn đã biết là chương trình không chạy. – Nếu bạn biết lý do tại sao chương trình không chạy, bạn có thể sửa được chương trình
cho nó chạy
– Nếu bạn hiểu chương trình của bạn, bạn sẽ có ít sai lầm và dễ dàng sửa chữa sai sót
hơn. Bí quyết là viết mã đơn giản, hiệu quả, chú thích hợp lý.
• Đối với mã nguồn, tiêu chí nào quan trọng hơn: rõ ràng hay chính xác ?
– Nếu mã nguồn rõ ràng, bạn có thể làm cho chương trình trở nên chính xác. – Bạn có chắc là làm cho chương trình trở nên chính xác nếu nó không rõ ràng hay
không ?
• Nếu chương trình được thiết kế với cấu trúc tốt, được viết bằng phong cách lập trình tốt và áp dụng các kỹ thuật viết chương trình hiệu quả, bẫy lỗi thì chi phí cho việc gỡ rối sẽ được giảm thiểu.
2. Tìm kiếm và gỡ rối
• Khi có lỗi, ta thường đổ cho trình dịch, thư viện hay bất cứ nguyên
nhân khách quan nào khác …tuy nhiên, cuối cùng thì lỗi vẫn là lỗi của CT, và trách nhiệm gỡ rối thuộc về LTV Phải hiểu vấn đề xuất phát từ đâu thì mới giải quyết được:
•
– Lỗi xảy ra ở đâu? Hầu hết các lỗi thường đơn giản và dễ tìm. Hãy khảo sát các
đầu mối và cố gắng xác định được đoạn mã nguồn gây lỗi
– Lỗi xảy ra như thế nào? Khi đã có 1 số thông tin về lỗi và nơi xảy ra lỗi, hãy
suy nghĩ xem lỗi xảy ra như thế nào
– Đâu là nguyên nhân gây lỗi? Suy luận ngược trở lại trạng thái của CT để xác
định nguyên nhân gây ra lỗi
• Gỡ rối liên quan đến việc suy luận lùi, giỗng như phá án. 1 số vấn đề không thể xảy ra và chỉ có những thông tin xác thực mới
đáng tin cậy
Phải đi ngược từ kết quả để khám phá nguyên nhân, Khi có lời giải thích đầy đủ, ta sẽ biết được vấn đề cần sửa và có thể
phát hiện ra 1 số vấn đề khác
2. Tìm kiếm và gỡ rối
Debugging Heuristic Áp dụng khi nào
(2) Nghĩ trước khi viết lại chương trình
(1) Hiểu các thông báo lỗi (error messages) Build-time (dịch)
(3) Tìm kiếm các lỗi (bug) hay xảy ra
(4) Divide and conquer Run-time (chạy)
(5) Viết thêm các đoạn mã kiểm tra để chương trình tự kiểm tra nó
(6) Hiện thị kết quả
(7) Sử dụng debugger
(8) Tập trung vào các lệnh mới viết / mới viết lại
2.1. Hiểu các thông báo lỗi
Gỡ rối khi dịch (build-time) CT dễ hơn gỡ rối khi chạy CT nếu LTV hiểu được các thông báo lỗi Một số thông báo lỗi đến từ preprocessor
Gõ sai tên file cần gọi
Thiếu dấu */
#include
Hiểu các thông báo lỗi
• Một số thông báo lỗi đến từ compiler
#include
int main(void)
/* Print "hello, world" to stdout and
return 0. */
{
printf("hello, world\n")
retun 0;
}
Hiểu các thông báo lỗi
• Một số thông báo lỗi đến từ linker
#include
int main(void)
/* Print "hello, world" to stdout and
return 0. */
{
prinf("hello, world\n")
return 0;
}
2.2. Nghĩ trước khi viết lại chương trình
Việc thay đổi mã nguồn không hợp lý có thể gây ra nhiều vấn đề hơn là để nguyên không thay đổi gì, nên…
Phải nghĩ trước khi làm
Nghĩ trước khi viết lại chương trình
• Gỡ rối ngay khi gặp
– Khi phát hiện lỗi, hãy sửa ngay, đừng để sau mới sửa, vì có thể lỗi
không xuất hiện lại (do tình huống)
– Cân nhắc: việc sửa chữa này có ảnh hưởng tới các tình huống khác
hay không ?
• Quan sát lỗi từ góc độ khác
– Viết đoạn mã nguồn gây lỗi ra giấy
• Đừng chép hết cả đoạn không có nguy cơ gây lỗi, hoặc in toàn bộ
code ra giấy in => phá vỡ cây cấu trúc
– Vẽ hình minh họa các cấu trúc dữ liệu
• Nếu mà giải thuật làm thay đổi CTDL, vẽ lại hình trước khi viết lại
giải thuật – Đọc trước khi gõ vào
• Đừng vội vàng, khi không rõ điều gì thực sự gây ra lỗi và sửa
không đúng chỗ sẽ có nguy cơ gây ra lỗi khác
Nghĩ trước khi viết lại chương trình
• Tạm dừng viết CT
– Khi gặp vấn đề, khó khăn, chậm tiến độ, lập tức thay đổi công việc =>
rút ra khỏi luồng quán tính sai lầm …
– Bỏ qua đoạn CT có lỗi – Khi nào cảm thấy sẵn sàng thì chữa • Giải thích logic của đoạn mã nguồn:
– Cho chính bạn
• Tạo điều kiện để suy nghĩ lại
– Cho ai khác có thể phản bác
• Extrem programming : làm việc theo cặp, pair programming,
người này LT, người kia kiểm tra, và ngược lại
– Cho cái gì đó không thể phản bác (cây, cốc trà đá, gấu bông, cún,…)
• Tạo điều kiện củng cố suy luận của mình
2.3. Tìm kiếm các lỗi hay xảy ra
• Xem lại các quy tắc lập trình
int i; … scanf("%d", i);
char c; … c = getchar();
switch (i) { case 0: … /* missing break */ case 1: … break; … }
if (i = 5) …
while (c = getchar() != EOF) …
if (5 < i < 10) …
if (i & j) …
Chú ý: nếu đặt chế độ cảnh báo (warnings) khi dịch thì hầu hết các lỗi kiểu này sẽ được phát hiện
Tìm kiếm các lỗi hay xảy ra
• Khi gặp vấn đề, hãy liên tưởng đến những trường
hợp tương tự đã gặp – Vd1 : int n; scanf(“%d”,n); ? – Vd2 : int n=1; double d=PI; printf(“%d %f \n”,d,n); ??
• Không khởi tạo biến (với C) cũng sẽ gây ra những
lỗi khó lường.
Tìm kiếm các lỗi hay xảy ra
• Làm cho lỗi xuất hiện lại
– Cố gắng làm cho lỗi có thể xuất hiện lại khi cần – Nếu không được, thì thử tìm nguyên nhân tại sao lại
không làm cho lỗi xuất hiện lại
Các phương pháp gỡ rối
• Tránh mắc cùng 1 lỗi 2 lần : Sau khi sửa 1 lỗi, hãy suy nghĩ
xem có lỗi tương tự ở nơi nào khác không. VD :
break;
case ‘o’ : /* tên tệp ouput*/ outname = argv[i]; break; case ‘f’ : from = atoi(argv[i]); break; case ‘t’ : to = atoi(argv[i]); break;
for (i=1;i
2.4. Divide and Conquer
• Thu hẹp phạm vi
• Tập trung vào dữ liệu gây lỗi
Divide and Conquer
– Thử chương trình với các tham số đầu vào từ đơn giản đến phức
tạp, từ nhỏ đến lớn để tìm lỗi
– Ví dụ: chương trình lỗi với file đầu vào filex
• Tạo ra phiên bản copy của filex , tên là filexcopy
• Xoá bớt nửa sau của filexcopy
• Chạy chương trình với tham số đầu vào là filexcopy
– Nếu chạy thông => nửa đầu của filex không gây lỗi, loại bỏ nửa này,
tìm lỗi trong nửa sau của filex
– Nếu không chạy => nửa đầu của filex gây lỗi, loại bỏ nửa sau, tìm lỗi
trong nửa đầu của filex
• Lặp cho đến khi không còn dòng nào trong filex có thể bị loại bỏ
– Cách khác: bắt đầu với 1 ít dòng trong filex, thêm dần các dòng
vào cho đến khi thấy lỗi
• Khử đầu vào
Divide and Conquer
• Khử mã nguồn
– Thử chương trình với các đoạn mã nguồn từ ngắn đến
dài để tìm lỗi
– Example: đoạn chương trình có sử dụng đến các phần
• Viết riêng từng lời gọi hàm trong đoạn chương trình bị lỗi
(test client)
– Hoặc viết thêm phần kiểm tra gọi hàm vào phần CTC được gọi
• Chạy thử test client
• Không chạy => lỗi liên quan đến việc gọi/ thực hiện CTC
vừa thử
• Chạy thông => lỗi nằm trong phần còn lại, tiếp tục thử gọi
các hàm khác
CTC khác không chạy
2.5. Viết thêm các đoạn mã kiểm tra để
chương trình tự kiểm tra nó
• Dùng internal test để khử lỗi trong CT và giảm
nhẹ công việc tìm kiếm lỗi
– Chỉ cần viết thêm 1 hàm để kiểm tra, gắn vào trước và
sau đoạn có nguy cơ, comment lại sau khi đã xử lý xong
lỗi
đã học
• Kiểm tra các giá trị không thay đổi
• Kiểm tra các đặc tính cần bảo lưu
• Kiểm tra giá trị trả về của hàm
• Thay đổi mã nguồn tạm thời
• Kiểm tra mà không làm thay đổi mã nguồn
• Dùng assertion để nhận dạng các lỗi có trong CT
– Các kỹ thuật viết thêm mã tự kiểm tra cho chương trình
Assertions
• Assertion: 1 macro hay 1 CT con dùng trong quá trình
phát triển ứng dụng, cho phép CT tự kiểm tra khi
chạy.
• Return true >> OK, false >> có 1 lỗi gì đó trong CT.
• Ghi lại những giả thiết được đưa ra trong code
• Loại bỏ những điều kiện không mong đợi
• VD :
– Nếu hệ thống cho rằng file dữ liệu về khách hàng không
bao giờ vượt quá 50 000 bản ghi, CT có thể chứa 1
assertion rằng số bản ghi là <= 50 000.
– Khi mà số bản ghi <= 50,000, assertion sẽ không có phản
ứng gì.
– Nếu đếm đc hơn 50 000 bản ghi, nó sẽ lớn tiếng “khẳng
định” rằng có 1 lỗi trong CT
Assertions
• Assertions có thể được dùng để kiểm tra các giả thiết
như :
– Các tham số đầu vào nằm trong phạm vi mong đợi (tương
tự với các tham số đầu ra)
– File hay stream đang được mở (hay đóng) khi 1 CTC bắt
đầu thực hiện (hay kết thúc)
– 1 file hay stream đang ở bản ghi đầu tiên (hay cuối cùng)
khi 1 CTC bắt đầu ( hay kết thúc) thực hiện
– 1 file hay stream được mở để đọc, để ghi, hay cả đọc và ghi
– Giá trị của 1 tham số đầu vào là không thay đổi bởi 1 CTC
– 1 pointer là non-NULL
– 1 mảng đc truyền vào CTC có thể chứa ít nhất X phần tử
– 1 bảng đã đc khởi tạo để chứa các giá trị thực
– 1 danh sách là rỗng (hay đầy) lkhi 1 CTC bắt đầu (hay kết
thúc) thực hiện
Assertions
• End users không cần thấy
các thông báo của assertion ;
• Assertions chủ yếu đc dùng
trong quá trình phát triển
hay bảo dưỡng ứng dụng.
• Dịch thành code khi phát
triển, loại bỏ khỏi code trong
sản phẩm để nâng cao hiệu
năng của chương trình
• Rất nhiều NNLT hỗ trợ
assertions : C++, Java và
Visual Basic.
VD:
#define ASSERT(condition, message)
{
if ( !(condition) ) {
fprintf(
stderr,
"Assertion %s failed: %s\n",
condition,
message);
exit( EXIT_FAILURE );
}
}
• Kể cả khi NNLT không hỗ trợ,
thì cũng có thể dễ dàng xây
dựng
Dùng assertions như thế nào ?
• Bẫy lỗi cho những tình huống lường trước (sự
kiện ta chờ đợi sẽ xảy ra);
– Error-handling : checks for bad input data Hướng tới
• Dùng assertions cho các tình huống không lường
trước (sự kiện không mong đợi xảy ra hoặc không
bao giờ xảy ra)
– Assertions : check for bugs in the code hướng đến
việc xử lý lỗi
• Tránh đưa code xử lý vào trong assertions
– Điều gì xảy ra khi ta turn off the assertions ?
việc hiệu chỉnh chương trình, tạo ra phiên bản mới của
chương trình
Dùng assertions như thế nào ?
• Các chương trình lớn:
– trước tiên xác nhận lỗi (dùng assertion),
– sau đó bẫy lỗi (dùng error-handling)
• Nguyên nhân gây lỗi đã được xác định:
– hoặc dùng assertion, hoặc dùng error-handling,
– không dùng cả 2 cùng lúc
• Các chương trình cực lớn, nhiều người cùng phát triển
trong thời gian 5-10 năm, hoặc hơn nữa ?
– Cả assertions và error handling code có thể đc dùng cho
cùng 1 lỗi.
– Ví dụ trong source code cho Microsoft Word, những điều
kiện luôn trả về true thì đc dùng assertion, nhưng đồng thời
cũng đc xử lý.
– Assertions rất có lợi vì nó giúp loại bỏ rất nhiều lỗi trong
quá trình phát triển hệ thống
Kiểm tra các giá trị không thay đổi
• Có thể sử dụng assert để kiểm tra các giá trị không thay đổi
#ifndef NDEBUG
int isValid(MyType object) {
…
Test invariants here.
Return 1 (TRUE) if object passes
all tests, and 0 (FALSE) otherwise.
…
}
#endif
void myFunction(MyType object) {
assert(isValid(object));
…
Manipulate object here.
…
assert(isValid(object));
}
2.6. Hiện thị kết quả đầu ra
•
In giá trị của các biến tại các
điểm có khả năng gây lỗi để
định vị khu vực gây lỗi, hoặc
• Xác định tiến trình thực hiện :
“đến đây 1”
• Poor:
stdout is buffered;
CT có thể có lỗi
trước khi hiện ra
output
printf("%d", keyvariable);
• Maybe better:
printf("%d\n", keyvariable);
• Better:
In '\n' sẽ xóa bộ nhớ
đệm stdout , nhưng
sẽ không xóa khi in
ra file
Gọi fflush() để làm
sạch buffer 1 cách
tường minh
printf("%d", keyvariable);
fflush(stdout);
Hiện thị kết quả đầu ra
• Tạo log file
• Lưu vết
– Giúp ghi nhớ đc các vấn đề đã xảy ra,
và giải quyết các vđề tương tự sau
này, cũng như khi chuyển giao CT
cho người khác..
• Maybe even better:
In debugging
output ra stderr;
debugging output
có thể tách biệt
với đầu ra thông
thường bằng cách
in ấn của CT
fprintf(stderr, "%d", keyvariable);
• Maybe better still:
Ghi ra 1 a log file
Ngoài ra: stderr
không dùng buffer
FILE *fp = fopen("logfile", "w");
…
fprintf(fp, "%d", keyvariable);
fflush(fp);
2.7. Sử dụng trình gỡ rối (debugger)
IDE : kết hợp soạn thảo, biên dịch, gỡ rối …
•
• Các trình gỡ rối với giao diện đồ họa cho phép chạy chương
trình từng bước qua từng lệnh hoặc từng hàm, dừng ở những
dòng lệnh đặc biệt hay khi xuất hiện những đk đặc biệt, bên
canh đó có các công cụ cho phép định dạng và hiển thị giá trị
các biến, biểu thức
• Trình gỡ rối có thể được kích hoạt trực tiếp khi có lỗi hoặc gắn
vào chương trình đang chạy.
• Thường để tìm ra lỗi , ta phải xem xét thứ tự các hàm đã đc
kích hoạt ( theo vết) và hiển thị các giá trị các biến liên quan
• Nếu vẫn không phát hiện đc lỗi : dùng các BreakPoint hoạc
chạy từng bước – step by step
• Có nhiều công cụ gỡ rối mạnh và hiệu quả, tại sao ta vẫn mất
nhiều thời gian và trí lực để gỡ rối ?
• Nhiều khi các công cụ không thể giúp dễ dàng tìm lỗi, nếu đưa
ra 1 câu hỏi sai, trình gỡ rối sẽ cho 1 câu trả lời, nhưng ta có
thể không biết là nó đang bị sai
Sử dụng trình gỡ rối
• Dùng 1 trình gỡ rối để chạy từng bước là phương sách cuối cùng
• Nhiều khi vấn đề tưởng quá đơn giản nhưng lại không phát hiện được,
ví dụ các toán tử so sánh trong pascal va VB có độ ưu tiên ngang
nhau, nhưng với C ?
( == và != nhỏ hơn !)
Thứ tự các đối số của lời gọi hàm : ví dụ : strcpy(s1,s2)
•
•
int m[6]={1,2,3,4,5,6}, *p,*q;
p=m; q=p+2; *p++ =*q++; *p=*q; ???
Lỗi loại này khó tìm vì bản thân ý nghĩ của ta vạch ra 1 hướng suy
nghĩ sai lệch : coi điều không đúng là đúng
• Đôi khi lỗi là do nguyên nhân khách quan : Trình biên dịch, thư viện
hay hệ điều hành, hoặc lỗi phần cứng : 1994 lỗi xử lý dấu chấm
độngng trong bộ xử lý Pentium
Công cụ hỗ trợ gỡ rối
GDB: The GNU Project Debugger
• Gỡ rối được các chương trình viết bằng Ada, C,
C++, Objective-C, Pascal, v.v., chạy trên cùng
máy cài đặt GDB hay trên máy khác
• Hoạt động trên nền UNIX và Microsoft Windows
• Các chức năng hỗ trợ:
– Bắt đầu chương trình, xác định những yếu tố làm ảnh
hưởng đến hoạt động của chương trình
– Dừng chương trình với điều kiện biết trước
– Khi chương trình bị dừng, kiểm tra những gì đã xảy ra
– Thay đổi các lệnh trong chương trình để LTV có thể thử
nghiệm gỡ rối từng lỗi một
2.8. Tập trung vào các lệnh mới viết /
mới viết lại
• Kiểm tra sự thay đổi mới nhất
– Lỗi thường xảy ra ở những đoạn CT mới được bổ sung
– Nếu phiên bản cũ OK, phiên bản mới có lỗi => lỗi chắc
chắn nằm ở những đoạn CT mới
– Lưu ý, khi sửa đổi, nâng cấp : hãy giữ lại phiên bản cũ –
đơn giản là comment lại đoạn mã cũ
– Đặc biệt, với các hệ thống lớn, làm việc nhóm thì việc sử
dụng các hệ thống quản lý phiên bản mã nguồn và các
cơ chế lưu lại quá trình sửa đổi là vô cùng hữu ích (
source safe )
Tập trung vào các lệnh mới viết / mới
viết lại
• Các lỗi xuất hiện thất thường :
– Khó giải quyết
– Thường gán cho lỗi của máy tính, hệ điều hành …
– Thực ra là do thông tin của chính CT : không phải do thuật
toán, mà do thông tin bị thay đổi qua mỗi lần chạy
– Các biến đã đc khởi tạo hết chưa ?
– Lỗi cấp phát bộ nhớ ? Vd :
char *vd( char *s) {
char m[101];
strncpy(m,s,100)
return m;
}
– Giải phóng bộ nhớ động ?
– for (p=listp; p!=NULL; p=p->next) free(p) ; ???
Tập trung vào các lệnh mới viết / mới viết lại
• Phải gỡ rối ngay, không nên để sau
– Khó: Viết toàn bộ chương trình; kiểm tra toàn bộ
chương trình, gỡ rối toàn bộ chương trình
– Dễ hơn: Viết từng đoạn, kiểm tra từng đoạn, gỡ rối
• Nên giữ lại các phiên bản trước
từng đoạn; viết từng đoạn, kiểm tra từng đoạn, gỡ rối
từng đoạn;
– Khó: Thay đổi mã nguồn, đánh dấu các lỗi; cố gắng nhớ
xem đã thay đổi cái gì từ lần làm việc trước
– Dễ hơn: Backup mã nguồn, thay đổi mã nguồn, đánh
dấu các lỗi; so sánh phiên bản mới với phiên bản cũ để
xác định các điểm thay đổi
Giữ lại các phiên bản trước đó
• Cách 1: Sao chép bằng tay vào 1 thư mục
…
$ mkdir myproject
$ cd myproject
Create project files here.
$ cd ..
$ cp –r myproject myprojectDateTime
$ cd myproject
Continue creating project files here.
…
• Lặp lại mỗi lần có phiên bản mới
• Cách 2: dùng công cụ như RCS (Revision Control
System), CVS, v.v.
Tóm lại
• Gỡ rối là 1 nghệ thuật mà ta phải luyện tập
thường xuyên
• Nhưng đó là nghệ thuật mà ta không muốn
• Mã nguồn viết tốt có ít lỗi hơn và dễ tìm hơn
• Đầu tiên phải nghĩ đến nguồn gốc sinh ra lỗi
• Hãy suy nghĩ kỹ càng, có hệ thống để định vị khu
vực gây lỗi
• Không gì bằng học từ chính lỗi của mình – điều
này càng đúng đối với LTV
2.4. Divide and Conquer
• Thu hẹp phạm vi • Tập trung vào dữ liệu gây lỗi
Divide and Conquer
– Thử chương trình với các tham số đầu vào từ đơn giản đến phức
tạp, từ nhỏ đến lớn để tìm lỗi
– Ví dụ: chương trình lỗi với file đầu vào filex
• Tạo ra phiên bản copy của filex , tên là filexcopy
• Xoá bớt nửa sau của filexcopy
• Chạy chương trình với tham số đầu vào là filexcopy
– Nếu chạy thông => nửa đầu của filex không gây lỗi, loại bỏ nửa này,
tìm lỗi trong nửa sau của filex
– Nếu không chạy => nửa đầu của filex gây lỗi, loại bỏ nửa sau, tìm lỗi
trong nửa đầu của filex
• Lặp cho đến khi không còn dòng nào trong filex có thể bị loại bỏ
– Cách khác: bắt đầu với 1 ít dòng trong filex, thêm dần các dòng
vào cho đến khi thấy lỗi
• Khử đầu vào
Divide and Conquer
• Khử mã nguồn
– Thử chương trình với các đoạn mã nguồn từ ngắn đến
dài để tìm lỗi
– Example: đoạn chương trình có sử dụng đến các phần
• Viết riêng từng lời gọi hàm trong đoạn chương trình bị lỗi
(test client)
– Hoặc viết thêm phần kiểm tra gọi hàm vào phần CTC được gọi
• Chạy thử test client • Không chạy => lỗi liên quan đến việc gọi/ thực hiện CTC
vừa thử
• Chạy thông => lỗi nằm trong phần còn lại, tiếp tục thử gọi
các hàm khác
CTC khác không chạy
2.5. Viết thêm các đoạn mã kiểm tra để chương trình tự kiểm tra nó
• Dùng internal test để khử lỗi trong CT và giảm
nhẹ công việc tìm kiếm lỗi – Chỉ cần viết thêm 1 hàm để kiểm tra, gắn vào trước và
sau đoạn có nguy cơ, comment lại sau khi đã xử lý xong lỗi
đã học
• Kiểm tra các giá trị không thay đổi • Kiểm tra các đặc tính cần bảo lưu • Kiểm tra giá trị trả về của hàm • Thay đổi mã nguồn tạm thời • Kiểm tra mà không làm thay đổi mã nguồn
• Dùng assertion để nhận dạng các lỗi có trong CT
– Các kỹ thuật viết thêm mã tự kiểm tra cho chương trình
Assertions
• Assertion: 1 macro hay 1 CT con dùng trong quá trình phát triển ứng dụng, cho phép CT tự kiểm tra khi chạy.
• Return true >> OK, false >> có 1 lỗi gì đó trong CT. • Ghi lại những giả thiết được đưa ra trong code • Loại bỏ những điều kiện không mong đợi • VD :
– Nếu hệ thống cho rằng file dữ liệu về khách hàng không
bao giờ vượt quá 50 000 bản ghi, CT có thể chứa 1 assertion rằng số bản ghi là <= 50 000.
– Khi mà số bản ghi <= 50,000, assertion sẽ không có phản
ứng gì.
– Nếu đếm đc hơn 50 000 bản ghi, nó sẽ lớn tiếng “khẳng
định” rằng có 1 lỗi trong CT
Assertions
• Assertions có thể được dùng để kiểm tra các giả thiết
như : – Các tham số đầu vào nằm trong phạm vi mong đợi (tương
tự với các tham số đầu ra)
– File hay stream đang được mở (hay đóng) khi 1 CTC bắt
đầu thực hiện (hay kết thúc)
– 1 file hay stream đang ở bản ghi đầu tiên (hay cuối cùng)
khi 1 CTC bắt đầu ( hay kết thúc) thực hiện
– 1 file hay stream được mở để đọc, để ghi, hay cả đọc và ghi – Giá trị của 1 tham số đầu vào là không thay đổi bởi 1 CTC – 1 pointer là non-NULL – 1 mảng đc truyền vào CTC có thể chứa ít nhất X phần tử – 1 bảng đã đc khởi tạo để chứa các giá trị thực – 1 danh sách là rỗng (hay đầy) lkhi 1 CTC bắt đầu (hay kết
thúc) thực hiện
Assertions
• End users không cần thấy
các thông báo của assertion ;
• Assertions chủ yếu đc dùng trong quá trình phát triển hay bảo dưỡng ứng dụng.
• Dịch thành code khi phát
triển, loại bỏ khỏi code trong sản phẩm để nâng cao hiệu năng của chương trình • Rất nhiều NNLT hỗ trợ
assertions : C++, Java và Visual Basic.
VD: #define ASSERT(condition, message) { if ( !(condition) ) { fprintf( stderr, "Assertion %s failed: %s\n", condition, message); exit( EXIT_FAILURE ); } }
• Kể cả khi NNLT không hỗ trợ, thì cũng có thể dễ dàng xây dựng
Dùng assertions như thế nào ?
• Bẫy lỗi cho những tình huống lường trước (sự
kiện ta chờ đợi sẽ xảy ra); – Error-handling : checks for bad input data Hướng tới
• Dùng assertions cho các tình huống không lường
trước (sự kiện không mong đợi xảy ra hoặc không bao giờ xảy ra) – Assertions : check for bugs in the code hướng đến
việc xử lý lỗi
• Tránh đưa code xử lý vào trong assertions – Điều gì xảy ra khi ta turn off the assertions ?
việc hiệu chỉnh chương trình, tạo ra phiên bản mới của chương trình
Dùng assertions như thế nào ?
• Các chương trình lớn:
– trước tiên xác nhận lỗi (dùng assertion), – sau đó bẫy lỗi (dùng error-handling)
• Nguyên nhân gây lỗi đã được xác định:
– hoặc dùng assertion, hoặc dùng error-handling, – không dùng cả 2 cùng lúc
• Các chương trình cực lớn, nhiều người cùng phát triển
trong thời gian 5-10 năm, hoặc hơn nữa ? – Cả assertions và error handling code có thể đc dùng cho
cùng 1 lỗi.
– Ví dụ trong source code cho Microsoft Word, những điều
kiện luôn trả về true thì đc dùng assertion, nhưng đồng thời cũng đc xử lý.
– Assertions rất có lợi vì nó giúp loại bỏ rất nhiều lỗi trong
quá trình phát triển hệ thống
Kiểm tra các giá trị không thay đổi
• Có thể sử dụng assert để kiểm tra các giá trị không thay đổi
#ifndef NDEBUG int isValid(MyType object) { … Test invariants here. Return 1 (TRUE) if object passes all tests, and 0 (FALSE) otherwise. … } #endif void myFunction(MyType object) { assert(isValid(object)); … Manipulate object here. … assert(isValid(object)); }
2.6. Hiện thị kết quả đầu ra
•
In giá trị của các biến tại các điểm có khả năng gây lỗi để định vị khu vực gây lỗi, hoặc • Xác định tiến trình thực hiện :
“đến đây 1”
• Poor:
stdout is buffered; CT có thể có lỗi trước khi hiện ra output
printf("%d", keyvariable);
• Maybe better:
printf("%d\n", keyvariable);
• Better:
In '\n' sẽ xóa bộ nhớ đệm stdout , nhưng sẽ không xóa khi in ra file
Gọi fflush() để làm sạch buffer 1 cách tường minh
printf("%d", keyvariable); fflush(stdout);
Hiện thị kết quả đầu ra
• Tạo log file • Lưu vết
– Giúp ghi nhớ đc các vấn đề đã xảy ra, và giải quyết các vđề tương tự sau này, cũng như khi chuyển giao CT cho người khác.. • Maybe even better:
In debugging output ra stderr; debugging output có thể tách biệt với đầu ra thông thường bằng cách in ấn của CT
fprintf(stderr, "%d", keyvariable);
• Maybe better still:
Ghi ra 1 a log file
Ngoài ra: stderr không dùng buffer
FILE *fp = fopen("logfile", "w"); … fprintf(fp, "%d", keyvariable); fflush(fp);
2.7. Sử dụng trình gỡ rối (debugger)
IDE : kết hợp soạn thảo, biên dịch, gỡ rối …
• • Các trình gỡ rối với giao diện đồ họa cho phép chạy chương
trình từng bước qua từng lệnh hoặc từng hàm, dừng ở những dòng lệnh đặc biệt hay khi xuất hiện những đk đặc biệt, bên canh đó có các công cụ cho phép định dạng và hiển thị giá trị các biến, biểu thức
• Trình gỡ rối có thể được kích hoạt trực tiếp khi có lỗi hoặc gắn
vào chương trình đang chạy.
• Thường để tìm ra lỗi , ta phải xem xét thứ tự các hàm đã đc
kích hoạt ( theo vết) và hiển thị các giá trị các biến liên quan
• Nếu vẫn không phát hiện đc lỗi : dùng các BreakPoint hoạc
chạy từng bước – step by step
• Có nhiều công cụ gỡ rối mạnh và hiệu quả, tại sao ta vẫn mất
nhiều thời gian và trí lực để gỡ rối ?
• Nhiều khi các công cụ không thể giúp dễ dàng tìm lỗi, nếu đưa ra 1 câu hỏi sai, trình gỡ rối sẽ cho 1 câu trả lời, nhưng ta có thể không biết là nó đang bị sai
Sử dụng trình gỡ rối
• Dùng 1 trình gỡ rối để chạy từng bước là phương sách cuối cùng • Nhiều khi vấn đề tưởng quá đơn giản nhưng lại không phát hiện được, ví dụ các toán tử so sánh trong pascal va VB có độ ưu tiên ngang nhau, nhưng với C ?
( == và != nhỏ hơn !) Thứ tự các đối số của lời gọi hàm : ví dụ : strcpy(s1,s2) • •
int m[6]={1,2,3,4,5,6}, *p,*q; p=m; q=p+2; *p++ =*q++; *p=*q; ??? Lỗi loại này khó tìm vì bản thân ý nghĩ của ta vạch ra 1 hướng suy nghĩ sai lệch : coi điều không đúng là đúng
• Đôi khi lỗi là do nguyên nhân khách quan : Trình biên dịch, thư viện hay hệ điều hành, hoặc lỗi phần cứng : 1994 lỗi xử lý dấu chấm độngng trong bộ xử lý Pentium
Công cụ hỗ trợ gỡ rối GDB: The GNU Project Debugger
• Gỡ rối được các chương trình viết bằng Ada, C, C++, Objective-C, Pascal, v.v., chạy trên cùng máy cài đặt GDB hay trên máy khác
• Hoạt động trên nền UNIX và Microsoft Windows • Các chức năng hỗ trợ:
– Bắt đầu chương trình, xác định những yếu tố làm ảnh
hưởng đến hoạt động của chương trình – Dừng chương trình với điều kiện biết trước – Khi chương trình bị dừng, kiểm tra những gì đã xảy ra – Thay đổi các lệnh trong chương trình để LTV có thể thử
nghiệm gỡ rối từng lỗi một
2.8. Tập trung vào các lệnh mới viết / mới viết lại
• Kiểm tra sự thay đổi mới nhất
– Lỗi thường xảy ra ở những đoạn CT mới được bổ sung – Nếu phiên bản cũ OK, phiên bản mới có lỗi => lỗi chắc
chắn nằm ở những đoạn CT mới
– Lưu ý, khi sửa đổi, nâng cấp : hãy giữ lại phiên bản cũ –
đơn giản là comment lại đoạn mã cũ
– Đặc biệt, với các hệ thống lớn, làm việc nhóm thì việc sử dụng các hệ thống quản lý phiên bản mã nguồn và các cơ chế lưu lại quá trình sửa đổi là vô cùng hữu ích ( source safe )
Tập trung vào các lệnh mới viết / mới viết lại
• Các lỗi xuất hiện thất thường :
– Khó giải quyết – Thường gán cho lỗi của máy tính, hệ điều hành … – Thực ra là do thông tin của chính CT : không phải do thuật
toán, mà do thông tin bị thay đổi qua mỗi lần chạy
– Các biến đã đc khởi tạo hết chưa ? – Lỗi cấp phát bộ nhớ ? Vd : char *vd( char *s) { char m[101]; strncpy(m,s,100) return m; } – Giải phóng bộ nhớ động ? – for (p=listp; p!=NULL; p=p->next) free(p) ; ???
Tập trung vào các lệnh mới viết / mới viết lại
• Phải gỡ rối ngay, không nên để sau
– Khó: Viết toàn bộ chương trình; kiểm tra toàn bộ
chương trình, gỡ rối toàn bộ chương trình
– Dễ hơn: Viết từng đoạn, kiểm tra từng đoạn, gỡ rối
• Nên giữ lại các phiên bản trước
từng đoạn; viết từng đoạn, kiểm tra từng đoạn, gỡ rối từng đoạn;
– Khó: Thay đổi mã nguồn, đánh dấu các lỗi; cố gắng nhớ
xem đã thay đổi cái gì từ lần làm việc trước
– Dễ hơn: Backup mã nguồn, thay đổi mã nguồn, đánh dấu các lỗi; so sánh phiên bản mới với phiên bản cũ để xác định các điểm thay đổi
Giữ lại các phiên bản trước đó
• Cách 1: Sao chép bằng tay vào 1 thư mục
… $ mkdir myproject $ cd myproject Create project files here. $ cd .. $ cp –r myproject myprojectDateTime $ cd myproject Continue creating project files here. …
• Lặp lại mỗi lần có phiên bản mới
• Cách 2: dùng công cụ như RCS (Revision Control
System), CVS, v.v.
Tóm lại
• Gỡ rối là 1 nghệ thuật mà ta phải luyện tập
thường xuyên
• Nhưng đó là nghệ thuật mà ta không muốn • Mã nguồn viết tốt có ít lỗi hơn và dễ tìm hơn • Đầu tiên phải nghĩ đến nguồn gốc sinh ra lỗi • Hãy suy nghĩ kỹ càng, có hệ thống để định vị khu
vực gây lỗi
• Không gì bằng học từ chính lỗi của mình – điều
này càng đúng đối với LTV