Sinh dữ liệu kiểm thử cho PUT

Một phần của tài liệu SINH CA KIỂM THỬ THAM SỐ HÓA CHO CHƯƠNG TRÌNH JAVA (Trang 38)

Trong mục trước, ta đã trình bày về kiến trúc của một hệ thống kiểm thử sử dụng kỹ thuật thực thi tượng trưng động cũng như việc sửa đổi một chương trình để hỗ trợ việc thực thi tượng trưng động. Chương trình sau khi đã được sửa đổi để hỗ trợ thực thi tượng trưng sẽ đươc thực thi với những giá trị cụ thể. Như đã trình bày trong mục 2.1.3, ban đầu chương trình đã sửa đổi để hỗ trợ thực thi tượng trưng sẽ được thực thi với những giá trị được sinh ngẫu nhiên. Khi chương trình bắt đầu được thực thi lần đầu tiên thì một cây thực thi tượng trưng sẽ được khởi tạo và các đỉnh (node) của cây sẽ được sinh ra dựa vào các thông tin tượng trưng (các ràng buộc, giá trị tượng trưng của các đầu vào) thu được. Các đỉnh tương ứng với nhánh đi mà sự thực thi cụ thể đó đi theo sẽ được đánh dấu là đã thăm và các đỉnh mới được sinh ra tương ứng với các nhánh mà sự thực thi cụ thể không đi theo sẽ được đánh dấu là chưa được thăm. Sau khi lần thực thi đó kết thúc, thì TIS cần chọn ra những đỉnh mà được đánh dấu là chưa thăm để thu gom ràng buộc trên nhánh của cây thực thi chứa đỉnh đó và giải quyết ràng buộc đã thu gom được để sinh các giá trị cụ thể làm đầu vào cho lần thực thi tiếp theo. Quá trình sẽ được lặp lại cho tới khi không còn các đỉnh mới của cây thực thi được sinh ra mà được đánh dấu là chưa được thăm. Để có thể chọn ra được các đỉnh mà được đánh dấu là chưa thăm thì TIS cần sử dụng các kỹ thuật tìm kiếm khác nhau như tìm kiếm theo chiều sâu

(Depth-First Search), tìm kiếm theo chiều rộng (Breadth-First Search), tìm kiếm kinh nghiệm. Và các ràng buộc trên đoạn đường đi (path prefix) từ đỉnh gốc tới đỉnh được chọn đó sẽ được thu gom và giải quyết. Thuật toán (Hình 10) dưới đây mô tả việc TIS sinh ra các đầu vào cho chương trình dựa trên cây thực thi tượng trưng mà TIS quản lý.

1: T = newSET();// khởi tạo cây thực thi tượng trưng 2: S = chiến lược tìm kiếm;

3: while(T chưa được thăm hết){

//chọn được đỉnh n chưa được thăm bằng chiến lược S

4: m = n = S(T); 5: pc = true; 6: while (m ≠ T.root){ 7: pc = pc ∧ m.constraint; 8: m = m.parent; 9: } inputs = solve(pc);

10: if(isSatisfied(inputs)){//tìm ra được inputs thỏa mãn

11: execute(inputs);//inputs đưa tới Test Executor để thực thi 12: expand(n);//chọn đỉnh n để mở rộng tiếp

13: if(có lỗi thực thi)

14: reportError();

15: marked(n);// đánh dấu đỉnh n đã được thăm

}

Hình 10: Thuật toán sinh dữ liễu kiểm thử

Tuy nhiên, việc lưu trữ cây thực thi tượng trưng tốn rất nhiều bộ nhớ. Do đó những đỉnh đã được đánh dấu là đã thăm mà không có đỉnh con cần được gỡ bỏ.

Với Pex[30] thì các ràng buộc đường đi thu gom được không phải là dạng công thức logic đơn thuần như ta đã trình bày ở trên mà là dạng công thức logic bậc một (first-order logic fomulas)[2]. Chính nhờ việc xây dựng ràng buộc dạng này mà Pex có thể sử dụng một SMT solver (Z3) để xử lý các ràng buộc đó. Pex sinh dữ liệu kiểm thử cho PUT bằng việc xây dựng và quản lý các cây thực thi cục bộ (intraprocedural execution trees).

Ví dụ 2.5:

Để hiểu rõ hơn về ràng buộc mà Pex xây dựng ta xét ví dụ 2.5 ở trên. Hàm TestAbs gọi một hàm khác là hàm abs. Các cây thực thi cục bộ tương ứng với mỗi hàm được minh họa trong Hình 11. Các cạnh của cây được gán nhãn bởi các ràng buộc và các đỉnh (node) của cây đại diện cho sự thực thi của một câu lệnh trong chương trình sao cho đường đi từ đỉnh gốc (root) của cây tới đỉnh lá (leaf) tương ứng với một đường đi thực thi cục bộ (intraprocedural path). Hình 11(a) là một phần của cây thực thi cục bộ tương ứng với hàm abs minh họa cho việc gọi hàm abs với giá trị x=1. Với cây thực thi cục bộ này ta làm quen với khái niệm là đỉnh treo (dangling node). Đỉnh treo là đỉnh đại diện cho đường đi chưa được thực thi. Đỉnh treo giống như đỉnh mới được tạo ra mà được đánh dấu là chưa được thăm như ta đã trình bày trong phần trước. Với kỹ thuật thực thi tượng trưng động[13, 19], việc thám hiểm có thể đạt tới đích (đỉnh treo) cho trước bằng thám hiểm lười (lazy exploration) và cố gắng tránh những đường đi mà không đạt tới đích bằng thám hiểm tin cậy (relevant exploration). Giả sử rằng ta gọi hàm TestAbs với p=1 và q=1. Sự thực thi này sẽ đi theo nhánh true của câu lệnh if đầu tiên trong hàm abs (đỉnh 3) cũng như nhánh false của câu lệnh if trong TestAbs (đỉnh 10). Hình 11(a) và (b) biểu thị cho cây thực thi cục bộ của hàm abs và TestAbs trong trường hợp này. Ta muốn sinh đầu vào cho hàm TestAbs để có thể thực thi câu lệnh xác nhận (đỉnh 11). Kỹ thuật này có thể tìm ra đầu vào để việc thực thi đạt tới đích (đỉnh 11) mà không cần phải thám hiểm các đường đi chưa được thám hiểm trong hàm mức thấp hơn (hàm abs).

int abs (int x){

if( x > 0) return x; else if( x == 0) return 100; else return −x; }

void TestAbs(int p,int q){

int m = abs(p);

int n = abs(q);

if(m > n && p > 0)

assertfalse; }

Hình 11: Các cây thực thi cục bộ tương ứng với hàm abs và TestAbs

Ràng buộc cục bộ (local path constraint) của một đỉnh n trong cây thực thi cục bộ Tf của hàm f được định nghĩa bằng ràng buộc đường đi của đường đi w từ đỉnh vào (entry node) của hàm f tới câu lệnh đại diện bởi n. Ràng buộc đường đi của đỉnh n (localpc(n)) biểu thị bởi các ký hiệu đầu vào Pf của ƒ

localpc(n):= lpcn ∧ Dg( a)

với mỗi g( a) xuất hiện trong lpcn

Trong đó lpcn là biểu thức kết hợp của ràng buộc trên các cạnh của đường đi w từ gốc của cây thực thi cục bộ tới đỉnh n và Dg(a) đại diện cho kết quả (function sumary) của hàm g được gọi bởi ƒ với đầu vào a mà xuất hiện trong lpcn.

Khi hàm ƒ gọi hàm g trong thực thi tượng trưng thì giá trị trả về của lời gọi tới hàm g là một đầu vào tượng trưng của ƒ. Giá trị trả về của lời gọi tới hàm g được biểu thị bởi g( a) với a là các đối số mà giá trị biểu thị bởi các ký hiệu đầu vào của ƒ. Nếu giá trị trả về được sử dụng trong câu lệnh rẽ nhánh của f thì g( a) xuất hiện trong ràng buộc đường đi. Ký hiệu hàm g sẽ được hiểu như ký hiệu hàm chưa định nghĩa (uninterpreted function)[2] bởi bộ xử lý ràng buộc. Hàm chưa định nghĩa này được thông dịch bởi một dạng định đề (axiom) ∀x. g(x) = E[x], trong đó E[x] là một biểu thức liên quan tới biến x. Như ví dụ ở trên thì hàm abs có thể được định nghĩa như sau:

∀x. abs(x) = ITE(x > 0, x, ITE(x = 0, 100,−x)), (ITE biểu thị cho if-then-else).

Ta sử dụng definition-predicate Dg cho mỗi ký hiệu hàm để đại diện cho giá trị trả về của lời gọi hàm. Ta định nghĩa Dg bằng định đề (axiom) δg như sau:

δg= ∀ Pg.Pg. Dg(Pg) ⇔ V localpc(l) Λ ret(l) leaf l in Tg

trong đó ret(l)= Gl nếu l là đỉnh treo và ret(l)=Retg(l) trong trường hợp khác. Retg(l) đại diện cho giá trị trả về của g, đó là một biểu thức biểu thị bởi Pg , trên đường đi cục bộ đã được thám hiểm hết đại diện bởi đỉnh treo . Với mỗi đỉnh treo d thì Gd là một biến logic duy nhất đại diện cho d. Quay lại ví dụ trên, TestAbs thực thi với p=1,q=1 thì ràng buộc đường đi cục bộ của đỉnh n được gán nhãn 11 sẽ như sau:

localpc(n):=abs(p) > abc(q) Λ p > 0 Λ Dabs(p) Λ Dabs(q)

Với đầu vào trên thì chỉ đường đi với x > 0 mới được thám hiểm trong abs, có một đỉnh treo d (đỉnh 2) đại diện cho nhánh của câu lệnh điệu kiện mà chưa được thám hiểm. Dabs sau đó được định nghĩa bởi định đề δabs :

δabs= ∀ Dabs(x)  ITE((x > 0, abs(x) = x, Gd) Nếu tất cả các đường đi của abs đều được thám hiểm (Hình 11(b)):

δabs= ∀ Dabs(x)  (x ≤ 0 Λ x = 0 Λ abs(x) = 100)

Λ (x ≤ 0 Λ x = 0 Λ abs(x) = −x)Λ (x > 0 Λ abs(x) = x)

Chương 3: Sinh ca kiểm thử tham số hóa với JPF 3.1. Kiến trúc của JPF

JPF[28] là một máy ảo Java (JVM) đặc biệt được sử dụng như một bộ kiểm tra mô hình (model checker) cho Java bytecode. JPF được sử dụng để phát hiện lỗi (bugs) trong các chương trình Java. JPF phân tích tất cả các đường đi thực thi của một chương trình Java để tìm ra sự vi phạm (violations) như deaklock hay các ngoại lệ chưa được xử lý. Khi JPF tìm ra một lỗi thì nó báo cáo về toàn bộ đường đi thực thi mà dẫn tới lỗi được phát hiện. Không giống như các trình gỡ rối (debugger) thông thường, JPF ghi nhớ về mọi bước phân tích dẫn đến một phát hiện lỗi.

Có thể sử dụng JPF một cách linh hoạt dưới dạng giao diện dòng lệnh hoặc tích hợp vào trong các môi trường phát triển ứng dụng Java như Netbean, Eclipse. JPF là một ứng dụng Java mã nguồn mở cho phép ta cấu hình để sử dụng nó theo các cách khác nhau và mở rộng nó.

Máy ảo JPF được cài đặt tuân theo các đặc tả về máy ảo Java nhưng không giống như các máy ảo Java chuẩn khác máy ảo JPF có khả năng lưu trữ trạng thái (state storing), ghi nhớ trạng thái (state matching) và quay lui trong quá trình thực thi chương trình.

Quay lui (backtracking) có nghĩa là JPF có thể phục hồi lại trạng thái thực thi lúc trước để xem có còn những sự lựa chọn thực thi khác.

Ghi nhớ trạng thái (state matching) có nghĩa là đánh dấu các trạng thái đã thám hiểm mà không còn cần thiết nữa giúp JPF tránh được những sự thực thi không cần thiết. Trạng thái thực thi của một chương trình bao gồm heap và ngăn xếp tiến trình. Trong khi thực thi JPF kiểm tra mọi trạng thái mới có phải là các trạng thái đã được ghi nhớ. Nếu các trạng thái đã được ghi nhớ JPF sẽ không tiếp tục đi theo đường đi thực thi hiện hành và quay lui tới trạng thái gần nhất mà chưa được thám hiểm.

Với những chương trình lớn và phức tạp thì không gian trạng thái trong quá trình thực thi chương trình là rất lớn. Việc lưu trữ và thám hiểm tất cả các trạng thái không phải lúc nào cũng đạt được. JPF sử dụng các kỹ thuật khác nhau để giải quyết vấn đề này đó là các chiến lược tìm kiếm, các kỹ thuật để giảm thiểu số trạng thái chương trình và việc lưu trữ các trạng thái. Có các chiến lược tìm kiếm khác nhau được sử dụng như tìm kiếm theo chiều sâu (Depth-First Search), theo chiều rộng (Breadth-First Search), tìm kiếm kinh nghiệm.

Hình 12: Kiến trúc JPF

+ JPF được thiết kế gồm hai thành phần chính đó là đối tượng máy ảo (VM) và đối tượng tìm kiếm (Search Object).

VM là một bộ sinh trạng thái bằng việc thực thi các chỉ thị bytecode. Đối tượng VM có các phương thức chính đó là :

− forward():sinh ra một trạng thái mới

− backtrack():phục hồi lại trạng thái ngay trước đó − restore(): phục hồi lại một trạng thái tùy ý

Search Object là bộ điều khiển của VM. Search Object điều khiển việc thực thi trong JVM. Search Object chịu trách nhiệm chọn lựa trạng thái để VM tiếp tục quá trình thực thi bằng việc điều khiển VM để sinh ra trạng thái tiếp theo (foward) hoặc quay lui về trạng thái đã được tạo lúc trước. Theo mặc định, kỹ thuật tìm kiếm theo chiều sâu được sử dụng. Một kỹ thuật tìm kiếm dựa trên hàng đợi ưu tiên (tìm kiếm kinh nghiệm) cũng được cài đặt và ta có thể cấu hình để sử dụng nó. Phương thức tìm kiếm được cài đặt với một vòng lặp và chỉ dừng lại khi không gian trạng thái đã được thám hiểm hết hoặc tìm ra một sự vi phạm.

+ Các tính năng mở rộng:

Search Listener và VM Listener: Chúng được sử dụng để quan sát việc thực thi bên trong JPF mà không cần cài đặt lại các lớp của VM và Search.

Search Listener được đăng ký tới các sự kiện của đối tượng Search:

public interface SearchListener { // got the next state

void stateAdvanced (Search search);

//state was backtracked one step

void stateBacktracked (Search search);

//a previously generated state was restored

void stateRestored (Search search); // JPF encountered a property violation

void propertyViolated (Search search); //a search loop was started

void searchStarted (Search search); // the search loop was finished

void searchFinished (Search search); }

và VM Listener được đặng ký tới các sự kiện của đối tượng VM trong quá trình thực thi một chương trình. VMListener được sử dụng để quản lý việc thực thi các chỉ thị bytecode:

public interface VMListener {

// VM has executed next instruction

void instructionExecuted (JVM vm);

// new Thread entered run() method

void threadStarted (JVM vm);

// Thread exited run() method

void threadTerminated (JVM vm);

// new class was loaded

void classLoaded (JVM vm);

// new object was created

void objectCreated (JVM vm);

// object was garbage collected

void objectReleased (JVM vm);

// garbage collection mark phase started

void gcBegin (JVM vm);

// garbage collection sweep phase terminated

void gcEnd (JVM vm);

// exception was thrown

void exceptionThrown (JVM vm);

// choice generator returned new value

void nextChoice (JVM vm);

... }

Model Java Interface (MJI): Nếu JNI là giao diện cho phép các ứng dụng Java chạy trong máy ảo Java của môi trường thực thi giao tiếp với các thư viện và ứng dụng

viết trong các ngôn ngữ khác như C, C++, Assembly của hệ điều hành bên dưới thì MJI là giao diện cho phép các ứng dụng Java chạy trong máy ảo JPF giao tiếp với máy ảo của môi trường thực thi bên dưới. Nói cách khác, MJI cho phép chuyển hướng việc thực thi một chương trình Java bên trong máy ảo JPF sang máy ảo của môi trường thực thi bên dưới (host JVM) mà JPF đang được chạy trên đó.

Bộ sinh lựa chọn (CG): JPF sử dụng CG để đạt tới các trạng thái chương trình mong muốn trong trường hợp có nhiều lựa chọn để từ một trạng thái có thể chuyển sang các trạng thái khác. Các trạng thái chương trình được bao gói trong đối tượng SystemState. Mỗi trạng thái bao gồm trạng thái thực thi hiện thời của VM (KernelState), các đối tượng ChoiceGenerator bao gói các sự lựa chọn cho việc chuyển đổi trạng thái (Transition). Bằng việc thực thi các chỉ thị bytecode chương trình có thể chuyển từ một trạng thái này sang một trạng thái khác. Mỗi trạng thái hiện hành của chương trình được đăng ký bởi một đối tượng ChoiceGenerator chứa các sự lựa chọn và trạng thái hiện hành này có thể chuyển tới trạng thái thực thi tiếp theo bằng việc truy vấn tới các giá trị của đối tượng ChoiceGenerator để lựa chọn dãy chỉ thị bytecode sẽ được thực thi tiếp theo.

Hình 13: Bộ sinh lựa chọn trong JPF

Bộ tạo chỉ thị (Bytecode Factory): Trong JPF có các lớp biểu thị cho tất cả các chỉ thị bytecode được cài đặt để thông dịch các chỉ thị bytecode đó sử dụng thư viện bcel[30]. InstructionFactory là giao diện cho phép tạo các đối tượng Instruction bao gói việc thực thi các chỉ thị bytecode. Khi một chỉ thị bytecode trong tập tin .class được tải

vào trong máy ảo JPF để xử lý thì một đối tượng Instruction sẽ được tạo ra. Nói cách khác, khi một tập tin .class được đọc một mảng các đối tượng Instruction sẽ được tạo ra. Các đối tượng Instruction có phương thức execute() để thực hiện việc thực thi chỉ thị bytecode tương ứng. Chế độ thực thi mặc định trong JPF là chế độ thực thi cụ thể. Với giao diện InstructionFactory, ta có khả năng ghi đè các lớp chỉ thị mà cài đặt việc thông dịch các chỉ thị bytecode để thay đổi chế độ thực thi trong JPF.

3.2. Symbolic JPF

Thực thi tương trưng một chương trình Java bằng cách cài đặt một trình thông dịch bytecode đặc biệt. Symbolic JPF là sự mở rộng của JPF bằng cách thông dịch bytecode theo ngữ nghĩa thực thi tượng trưng. Symbolic JPF không đòi hỏi phải sửa đổi chương trình mã nguồn như các cách tiếp cận thực thi tượng trưng một chương trình sử dụng JPF trước đây[16, 20, 26]. Với Symbolic JPF, việc thực thi tượng trưng với các phương thức mà gọi các phương thức khác sẽ được thực hiện dễ dàng.

3.2.1. Bộ tạo chỉ thị

Bộ tạo chỉ thị (Instuction Factory) cho phép thay thế và mở rộng ngữ nghĩa thực thi cụ thể của bytecode bằng ngữ nghĩa thực thi tượng trưng. Các thông tin tượng trưng được lưu trữ trong các thuộc tính (attributes) kết hợp với dữ liệu chương trình (fields, stack operands, local variables) và được cập nhật trong quá trình thực thi.

Thực thi các chương trình Java đã được biên dịch (.class files) bằng cách thông dịch các chỉ thị bytecode bằng một máy ảo Java (VM). Theo mặc định, VM thông dịch các bytecode theo ngữ nghĩa thực thi cụ thể. VM của JPF cũng dựa trên mô hình ngăn

Một phần của tài liệu SINH CA KIỂM THỬ THAM SỐ HÓA CHO CHƯƠNG TRÌNH JAVA (Trang 38)

Tải bản đầy đủ (DOC)

(68 trang)
w