JPF hoàn toàn là một chương trình Java có thể được chạy như là một công cụ dòng lệnh độc lập hoặc được nhúng vào các hệ thống khác như các môi trường phát triển. Lịch sử phát triển của JPF như sau:
- Năm 1999 : JPF1 là Java-to- Promela.
- Năm 2000: JPF2 là công cụ kiểm chứng mô hình được tùy biến có thể
hiểu mã bytecode của Java và sử dụng mã bytecode một cách trực tiếp. - Năm 2003: thiết kế và thực thi cấu trúc mở rộng.
- Năm 2005: mã nguồn của JPF được mở ra cho cộng đồng phát triển, - Năm 2006: JPF4 với bộ sinh hợp nhất và máy thực thi mới.
3.2 Những gì có thể đƣợc kiểm chứng bởi Java PathFinder
JPF là một nền tảng có thể mở rộng được. Do đó nhiều dự án đã được triển khai nhằm mở rộng những tính năng cho JPF. Hiện tại, các mở rộng của JPF bao gồm: kiểm chứng mô hình giao diện người dùng (User Interface Model Checking), sinh dữ liệu kiểm thử ký hiệu (Symbolic Test Data Generation), sinh kiểm thử luồng an toàn ký hiệu (Symbolic Threadsafety Test Generation), khung làm việc kiểm chứng cấu thành (Compositional Verification Framework), kiểm chứng đặc tính số (Numeric Property Verification), kiểm chứng mô hình biểu đồ trạng thái UML (UML State Chart Model Checking). Java Pathfinder có thể tìm các lỗi khóa chết (deadlock) và các ngoại lệ không được xử lý (unhandled exception) như NullPointerExceptions và AssertionErrors, tuy nhiên người dùng còn có thể tự cung cấp các lớp hoặc viết các phần mở rộng để thực hiện kiểm chứng các thuộc tính khác. JPF có thể thực hiện mô phỏng không tất định (non- determinism). Mô phỏng không tất định yêu cầu hệ thống sinh tất cả các lựa chọn có thể cho tất cả các trạng thái của hệ thống. Hệ thống này có hai khả năng:
17
backtracking và state matching. Hai cách để tạo ra mô phỏng không tất định trong JPF là quay lui (backtracking) và so khớp trạng thái (state matching).
3.3 Kiến trúc của Java PathFinder
JPF [15,16] là một máy ảo Java (JVM) được sử dụng để phát hiện lỗi (bugs) trong các chương trình Java. 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ó.
JPF được thiết kế xung quanh hai khái niệm trừu tượng chính: JPF được thiết kế gồm hai thành phần chính đó là: đối tượng máy ảo (JVM) và đối tượng tìm kiếm (Search Object).
Hình 3.1: Kiến trúc JPF
Máy ảo (JVM):
Máy ảo JVM của JPF là một bộ sinh trạng thái bằng việc thực thi các chỉ thị bytecode, nó có khả năng quản lý các trạng thái: quay lui, khớp trạng thái và máy ảo lưu trữ trạng thái. Có 3 phương thức JVM chính hợp tác giữa máy ảo và tìm kiếm (VM-Search):
18
forward() – sinh trạng thái tiếp theo, thông báo nếu trạng thái sinh ra là nút lá. Nếu đúng lưu trữ vào một ngăn xếp backtrack phục vụ phục hồi hiệu quả.
backtrack() – phục hồi trạng thái cuối trong ngăn xếp quay lui. restoreState() – phục hồi một trạng thái tùy ý.
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.
Đối tƣợng tìm kiếm (Search Object): là một cấu hình chiến lược tìm kiếm
đối tượng theo hướng thực thi trong máy ảo. Nói cách khác, đối tượng tìm kiếm lựa chọn các trạng thái mà từ đó JVM nên xử lý bằng cách trực tiếp JVM sinh ra trạng thái tiếp theo hoặc quay lại trạng thái được tạo ra trước đó. Một khả năng quan trọng của đối tượng tìm kiếm là nó có thể đánh giá đối tượng thuộc tính, chẳng hạn như: xác định một thuộc tính là không phải deadlock - phương thức
NotDeadlocked, hoặc đảm bảo rằng các thuộc tính không vi phạm sự khẳng định. – phương thức NoAssertionsViolatedProperty.
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.
19
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.
3.4 Một số phần mở rộng của Java PathFinder
JPF cung cấp một số phần mở rộng như sau:
Bytecode Factories: cho phép thay đổi ngữ nghĩa trong thực thi chỉ thị bytecode.
Listeners: theo dõi và kiểm soát thực thi chương trình JPF. Report: định dạng, hiển thị các đầu ra.
3.4.1 Bộ tạo chỉ thị (Bytecode Factory)
Hình 3.2: 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. 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
20
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. Lớp InstructionFactory là giao diện để 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ì đối tượng Instruction sẽ được tạo ra. Nói cách khác, khi một tập tin .class được đọc thì một mảng các đối tượng Instruction sẽ được tạo ra. Với mỗi phương thức được thực thi, JPF quản lý một đối tượng đó là MethodInfo. Đối tượng MethodInfo lưu trữ các chỉ thị bytecode như là mảng các đối tượng Instruction. 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.
JPF sử dụng mẫu thiết kế abstract factory[14] để khởi tạo các đối tượng Instruction. Lớp SymbolicInstructionFactory chứa các chỉ thị bytecode theo ngữ nghĩa thực thi tượng trưng. Lớp SymbolicInstructionFactory ủy quyền (delegate) tới lớp cha DefaultInstructionFactory chứa các chỉ thị bytecode theo nghĩa thực thi cụ thể.
3.4.2 Listeners
Listeners [15,12] là phần mở rộng quan trọng nhất của JPF, chúng cung cấp
Hình 3.3: JPF Listeners
cách để quan sát quá trình thực thi chương trình, tương tác và mở rộng JPF bằng các lớp do người dùng tự định nghĩa. Sau khi các lớp được thực thi Listener
21
không yêu cầu bất kỳ sửa đổi nào trong nhân của JPF. Listeners được thực thi cùng cấp với JPF.
Để sử dụng các tính năng mở rộng của Listener người dùng có thể cấu hình các thuộc tính trong file *.jpf hoặc cấu hình khi thực thi dòng lệnh. Các ứng dụng có thể sử dụng chú thích @JPFConfig để xác định rõ thuộc tính cho JPF Listeners.
Các loại Listeners
Search Listener và VM Listener: Chúng được sử dụng để quan sát việc
thực thi mã bên trong JPF mà không cần cài đặt lại các lớp của JVM và Search.
Hình 3.4: Các loại Listener
Search Listener: Được sử dụng để theo dõi quá trình tìm kiếm không gian
trạng thái, nó đượ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
22
// the search loop was finished
void searchFinished (Search search);
}
Các thuộc tính được sử dụng thường xuyên nhất là:
+ stateAdvanced - để lưu trữ bổ sung, thông tin trạng thái backtrackable trong một mảng.
+ stateBacktracked - để khôi phục lại thông tin trạng thái bổ sung. + searchFinished - để xử lý kết quả thu được.
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. VM Listener đượ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);
...
}
+ executeInstruction: được gọi trước khi một lệnh bytecode được thực hiện các máy ảo. Listener có thể sử dụng điều này để bỏ qua hoặc hoặc thay thế chỉ thị này.
23
+ instructionExecuted: thông báo sau khi thực hiện, phù hợp để theo dõi kết quả thực hiện (lời gọi đến phương thức, các giá trị gán, kết quả của các nhánh, …)
PropertyListenerAdapter được sử dụng trong trường hợp Listeners khi triển khai các thuộc tính, tức là có thể chấm dứt quá trình tìm kiếm. Một ví dụ nổi bật của thể loại này là PreciseRaceDetector.
ListenerAdapter là bộ chuyển đổi cho SearchListener, VMListener và PublisherExtension. Đây là nơi chủ yếu được sử dụng để thu thập thông tin trong quá trình thực hiện JPF (ví dụ sử dụng lớp CoverageAnalyzer hay DeadlockAnalyzer).
3.4.3 Hệ thống báo cáo (The Report)
Hệ thống báo cáo (The Report) [15,16] có chức năng nhận kết quả đầu ra, định dạng và hiển thị các kết quả khi thực thi JPF như báo cáo vi phạm thuộc tính, vết chương trình, các thống kê… Đây là một phần quan trọng nhất của giao diện người dùng JPF, nó có thể hiển thị các định dạng đầu ra khác nhau như: tệp văn bản, màn hình console, XML, các lời gọi API. Tùy thuộc vào ứng dụng và dự án, người dùng có thể thay đổi cách hiển thị bằng việc cấu hình lại các thuộc tính. Hệ thống báo cáo của JPF bao gồm 3 thành phần:
- Lớp Reporter
- Các đối tượng Publisher
- Các đối tượng PublisherExtension
Tất cả các lớp chính liên quan và các giao diện nằm trong gói gov.nasa.jpf.report. Trong đó:
- Đối tượng Reporter có nhiệm vụ thu gom dữ liệu. Nó quản lý và thông báo cho các phần mở rộng của Publisher khi dữ liệu đầu ra đã đạt tới ngưỡng.
- Đối tượng Publisher có nhiệm vụ ghi dữ liệu từ Reporter ra một định dạng nào đó (text, xml, html,…). Publisher thông dụng nhất là ConsolePublisher, nó có chức năng ghi dữ liệu ra dạng text, console hoặc ghi ra file.
-Đối tượng PublisherExtension sẽ được đăng ký với đối tượng Publisher khi chương trình bắt đầu chạy. Mục đích để có thể định nghĩa một PublisherExtension là kế thừa đối tượng ListenerAdaper, đăng ký Publisher Extension này với JPF trong hàm khởi tạo của nó.
-Một đối tượng khá quan trọng trong JPF đó chính là Config, đây là đối tượng lưu trữ tất cả các thuộc tính (properties) cấu hình của JPF. Các thuộc tính của JPF sẽ được định nghĩa trong file jpf.properties và *.jpf. Dưới đây là một số thuộc tính quan trọng của Report JPF:
24
report.class=gov.nasa.jpf.report.Reporter: chỉ định Report cho
chương trình
report.publisher=console,xml: chỉ định có thể báo cáo ở dạng
console hoặc xml
Hình 3.5: Hệ thống báo cáo Report
report.console.class=gov.nasa.jpf.report.ConsolePublisher:chỉ
định đối tượng để báo cáo ghi vết là ConsolePublisher.
report.console.property_violation=error,trace,snapshot: sẽ ghi
thông tin ở dạng error, stack trace vào snapshot.
report.console.file=My_JPF_report: chỉ định đường dẫn chứa file vết của
JPF
3.5 Đo độ phủ trong kiểm thử đơn vị sử dụng JPF CoverageAnalyzer
JPF là công cụ kiểm chứng phần mềm hỗ trợ nhiều tính năng phục vụ cho việc kiểm chứng. Một trong số đó chính là đo độ phủ của các ca kiểm thử với phương thức được thực thi. Phần mở rộng Listener là một phần mở rộng quan trọng, nó có nhiệm vụ theo dõi mã nguồn và kiểm soát thực thi chương trình. Để thực hiện đo độ phủ của bộ các ca kiểm thử chúng ta có thể sử dụng một tính năng trong phần mở rộng này cụ thể là sử dụng lớp CoverageAnalyzer. Lập trình viên sẽ cấu hình file config nhằm gọi sử dụng đối tượng của lớp
25
CoverageAnalyzer, đối tượng này sẽ đọc mã nguồn của một lớp cần kiểm tra kết hợp với các ca kiểm thử để kiểm thử các phương thức trong lớp. Qua quá trình xử lý kết quả đầu ra đã chỉ rõ số điều kiện đã phủ trên tổng số điều kiện theo tiêu chuẩn bao phủ điều kiện (condition coverage).
Hình 3.6: Quy trình kiểm tra độ phủ hàm sử dụng JPF CoverageAnalyzer
Trong lớp CoverageAnalyzer có tham số branch tham số này có chức năng đếm số nhánh đã được phủ dựa trên tiêu chuẩn bao phủ điều kiện (Condition coverage). Lấy ví dụ hàm exam như sau :
public static float exam(int a, int b, int c, int d) {
1: if (a == 0) 2: return 0; 3: int x = 0; 4: if ((a == b) || (c == d)) 5: x = 1; 6: return x + 2; }
Hình 3.7: Đồ thị luồng điều khiển của hàm exam Bảng 3.1: Bộ test theo tiêu chuẩn bao phủ điều kiện của hàm exam Bảng 3.1: Bộ test theo tiêu chuẩn bao phủ điều kiện của hàm exam
ID Test Path Input EO
Tc1 1, 2 0,1,2,3 0 Tc2 1,3,4T1(F),4T2(T),5,6 1,2,2,2 3 Tc3 1,3,4T1(T),5,6 1,1,2,3 3 Tc4 1,3,4T1(F),4T2(F),6 1,2,4,3 2 Sinh các ca kiểm thử Thực hiện các ca kiểm thử Đo độ phủ JPF CoverageAnalyzer Đơn vị chƣơng trình Tiêu chuẩn bao phủ Xác định các đƣờng đi Xây dựng đồ thị luồng điều khiển 2 3 5 4T1 4T2 6 1
26
Sau khi kiểm thử với các ca kiểm thử thỏa mãn tiêu chuẩn bao phủ điều kiện (condition coverage) trong bảng 3.1, đo độ phủ với JPF CoverageAnalyzer kết quả hiển thị trong hình 3.8 tại cột branch là 1.0 (3/3)
Hình 3.8: Minh họa kết quả đo độ phủ sử dụng JPF CoverageAnalyzer
JPF CoverageAnalyzer đo độ phủ bằng cách đếm số điều kiện cần kiểm tra trong hàm, nếu điều kiện phức tạp gồm nhiều điều kiện con chương trình sẽ đếm riêng rẽ từng điều kiện con đó. Nhìn vào hàm exam thấy được rằng có tất cả 3 quyết định (a==0) ,(a==b) và (c==d), ba quyết định này đều đã được phủ hết hai nhánh true, false mỗi nhánh ít nhất 1 lần cho mỗi điều kiện vì vậy độ đo thu được tại cột branch sẽ là 1.0 (3/3) như trong hình 3.8.
Tuy nhiên nhược điểm của phương pháp này đó là khi sử dụng tiêu chuẩn bao phủ câu lệnh (statement coverage) và bao phủ nhánh(branch coverage) kết quả không thực sự rõ ràng.
Khi đo độ bao phủ sử dụng tiêu chuẩn bao phủ lệnh tức là trong thiết kế các ca