Java PathFinder (JPF) [19, 25, 30, 31] là một hệ thống để kiểm chứng mã thực thi bytecode của các chương trình Java. Ở dạng đơn giản, nó là một máy ảo Java được sử dụng như một công cụ kiểm chứng mô hình trạng thái phần mềm tường minh, khảo sát tất cả các đường thực thi tiềm năng của một chương trình để tìm ra các vi phạm của các thuộc tính như khóa chết (deadlock) hay các ngoại lệ không được xử lý (unhanled exceptions). Không giống như các công cụ gỡ lỗi truyền thống, JPF thông báo đầy đủ đường thực thi dẫn đến một một lỗi. JPF đặc biệt rất phù hợp cho việc tìm kiếm các lỗi rất khó kiểm thử trong các chương trình đa luồng.
2.2.1. Lịch sử của Java PathFinder
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. Nó đã đi một chặng đường dài từ năm 1999. Có năm mốc chính cần chú ý sau:
1) Năm 1999 : JPF1 là Java-to-Promela. JPF chỉ là công cụ dùng để dịch mã nguồn Java sang ngôn ngữ Promela cho công cụ kiểm chứng mô hình SPIN.
2) 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.
3) Năm 2003: Thiết kế và thực thi cấu trúc mở rộng.
4) Năm 2005: Mã nguồn của JPF được mở ra cho cộng đồng phát triển.
5) Năm 2006: JPF4 với bộ sinh hợp nhất (unified ChoiceGenerators) và máy thực thi mới.
Trong suốt thời gian này, có rất nhiều người và tổ chức đã làm việc với JPF. Đa số các công việc vẫn đang được làm bởi nhóm Công nghệ Phần mềm Robust (RSE) ở Trung tâm Nghiên cứu Ames của NASA.
2.2.2. Các thành phần của Java PathFinder
Java, ngoài chức năng là một ngôn ngữ lập trình nó còn là một tập các tầng bắt đầu bằng một nền thực thi máy ảo cụ thể (“VM chủ”) nằm trên đỉnh của các thư viện thực thi (native libraries) được viết cho hệ điều hành. Đối với ngăn xếp này, chúng ta thêm JPF – một ứng dụng Java để chạy trên đỉnh của máy ảo chủ, nhưng bản thân nó là một máy áo (VM) mà sau đó thực thi hệ thống được kiểm thử (System Under Test – SUT).
Với tất cả sự đệ quy này, thật là rắc rối về việc xác định đoạn mã Java nào được xử lý tại mức nào. Chúng ta sẽ xem xét những thành phần nào liên quan khi áp JPF vào SUT. Chúng ta phân loại các thành phần theo hai khía cạnh:
1) Xử lý VM (VM chủ, JPF)
2) Thực thể phân phối liên kết (hệ thống Java chủ, lõi JPF, các mở rộng của JPF, SUT)
Sau đây là sơ đồ JPF chi tiết:
Hình 2.2. Các thành phần của JPF
Chúng ta sẽ đi từ bên trái sang phải sơ đồ này. Chúng ta bắt đầu từ ứng dụng Java được biên dịch mà chúng ta muốn kiểm chứng. Đây không phải là một phần của phân phối JPF nhưng sẽ được thực thi bởi JPF, do đó các tệp lớp tương ứng không cần phải thấy được đối với máy ảo chủ (chạy JPF). Tuy nhiên, mã ứng dụng có thể sử dụng các lớp và các giao diện hiện có trong phân phối JPF (ví dụ: các khung làm việc như việc mô hình hóa sơ đồ trạng thái UML?). Đồng thời, ứng dụng và các thư viện mô hình hóa và các diễn giải tạo thành hệ thống được kiểm thử (SuT).
Phần tiếp theo là lõi JPF. Từ trước đến giờ chúng ta biết đây là một máy ảo được viết bằng Java, do đó nó có thể được chạy trên bất kỳ hệ thống Java nào được cài đặt. Điều này có nghĩa là tất cả các lớp cấu thành JPF cần thấy được bởi máy ảo Java chủ (host JVM) chứ không phải là JPF. Ngoài việc phải thiết lập biến môi trường CLASSPATH, máy ảo chủ phải có đủ bộ nhớ để chạy (có thể cấu hình bởi tham số -
Xmx… trong Eclipse). JPF không phải là một hệ thống nguyên khối. Nó bao gồm nhiều cấu phần khác nhau được cấu hình để làm nhiều thứ.
Phần cuối cùng là phần phức tạp nhất. Các ứng dụng hầu hết sử dụng các thư viện chuẩn của Java (ví dụ System.out.println()) như các lớp cấu thành nên JPF làm. Một số lớp thư viện chuẩn phải được thay thế bởi các phiên bản đặc thù của JPF khi JPF thực thi hệ thống được kiểm chứng (SuT) và JPF có một cơ chế đặc biệt là MJI để làm việc đó. Nhưng nhiều lớp thư viện chuẩn là Java thuần túy và chúng ta lấy chúng thẳng từ máy ảo Java chủ. Điều này có nghĩa là các thư viện chuẩn nằm trong đường dẫn đến thư viện lớp của JPF và máy ảo Java chủ, nhưng các thực thể của lớp tương ứng không có gì chung – cái này tồn tại hoàn toàn bên trong JPF, cái kia hoàn toàn trong máy chủ ảo.
2.2.3. Những gì có thể được kiểm chứng bởi Java PathFinder
Những lỗi nào có thể được tìm thấy bởi JPF? Java 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. Các kiểm tra điều kiện tranh chấp (race condition) và các giới hạn bộ nhớ heap (heap bound) cũng được gói trong bản phân phối của JPF.
Chương trình nào có thể được kiểm chứng bởi JPF? Nói chung, JPF có khả năng kiểm chứng mọi chương trình Java không phụ thuộc vào các phương thức không được hỗ trợ nội tại. Máy ảo JPF không thể thực thi với một nền tảng đặc thù, mã riêng. Điều này áp đặt giới hạn những thư viện chuẩn nào có thể được sử dụng bên trong ứng dụng cần được kiểm thử. Mặc dù có thể viết ra các phiên bản thư viện này (đặc biệt bằng cách sử dụng Model Java Interface – MJI), nhưng nó hiện không hỗ trợ cho Java.awt, Java.net và chỉ giới hạn hỗ trợ cho Java.io và cơ chế Java‟s runtime reflection. Một hạn chế khác nữa liên quan đến các đòi hỏi lưu trữ trạng thái của JPF, nó giới hạn kích thước của ứng dụng có thể kiểm chứng khoảng 10kloc (10 ngàn dòng lệnh), phụ thuộc vào cấu trúc bên trong chương trình, nếu không có các trừu tượng hóa cụ thể đặc tính và ứng dụng được sử dụng. Vì sự hạn chế giới hạn kích cỡ, JPF cho đến nay được sử dụng chủ yếu cho các ứng dụng dạng mô hình, nhưng đòi hỏi một ngôn ngữ lập trình hướng thủ tục đầy đủ. JPF đặc biệt hữu ích để kiểm chứng các chương trình Java tương tranh, do có hệ thống nghiên cứu trình tự lập kế hoạch, một lĩnh vực đặc biệt khó khăn đối với việc kiểm thử truyền thống.
2.2.4. Kiểm chứng mô hình trong Java PathFinder
Đối lập với kiểm thử chỉ thực hiện một đường với một dữ liệu đầu vào, 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: 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).
1) Quay lui có nghĩa là JPF có thể khôi phục các trạng thái thực thi trước đó để xem xét các lựa chọn còn lại chưa được duyệt. Ví dụ, nếu JPF duyệt đến một trạng thái kết thúc chương trình thì nó có thể quay lui trở lại để tìm các chuỗi lịch trình có thể xảy ra khác vẫn chưa được thực thi. Trong khi điều này về mặt lý thuyết được thực hiện bằng việc thực thi lại chương trình từ đầu, quay lui là kỹ thuật hiệu quả hơn nhiều nếu việc lưu trữ trạng thái được tối ưu hóa.
2) So khớp trạng thái là một kỹ thuật khóa khác để tranh các công việc dư thừa. Trạng thái thực thi của một chương trình chủ yếu bao gồm các ảnh (snapshot) của bộ nhớ heap và ngăn xếp tiến trình (thread-stack). Trong khi JPF thực thi, nó kiểm tra mọi trạng thái mới nếu nó đã nhận thấy một trạng thái tương tự, trong trường hợp này không cần thiết phải tiếp tục theo đường thực thi hiện tại và có thể quay lui tới lựa chọn không tất định chưa được duyệt gần nhất.
Về lý thuyết, kiểm chứng mô hình trạng thái tường minh là phương pháp chính xác – tất cả các lựa chọn được duyệt, nếu có bất kỳ lỗi nào thì nó sẽ được phát hiện. Không may là kiểm chứng mô hình có thể chỉ phù hợp với các chương trình nhỏ, thường là nhỏ hơn 10 nghìn dòng lệnh. Số trạng thái sẽ tăng vượt qua các giới hạn tính toán một cách nhanh chóng đối với các chương trình phức tạp. Vấn đề này được biết đến như là sự bùng nổ không gian trạng thái và có thể dễ dàng được minh họa bằng các chuỗi lịch trình có thể xảy ra đối với một số tiến trình (process) cụ thể bao gồm các phân đoạn nguyên tử (atomic section).
Hình 2.3. Bùng nổ không gian trạng thái do sự đan xen giữa các luồng JPF giải quyết vấn đề hiệu năng này bằng ba cách: (1) các chiến lược tìm kiếm có thể cấu hình được, (2) giảm sổ lượng trạng thái, và (3) giảm các chi phí lưu trữ trạng thái.
1) Các chiến lược tìm kiếm cấu hình được thử giải quyết bài toán mà toàn bộ không gian trạng thái không thể được tìm kiếm bằng việc định hướng tìm kiếm cốt để các lỗi được tìm ra nhanh hơn, ví dụ với các nguồn tính toán ít hơn. Điều này về cơ bản nghĩa là sử dụng công cụ kiểm chứng mô hình không phải như là một công cụ chứng minh mà như là một công cụ gỡ lỗi mà hầu hết được thực hiện bằng việc sử
dụng các tìm kiếm kinh nghiệm (heuristic) để sắp xếp và lọc tập các trạng thái tiềm năng kế tiếp theo một số thuộc tính thích hợp liên quan. Sự tính toán các giá trị theo kinh nghiệm được giao phó cho một lớp được cấu hình của người dùng, tức là không được viết mã cố định trong nhân của JPF.
2) Việc làm giảm số trạng thái phải được lưu trữ là cách được ưu thích để cải thiện hiệu năng và được hỗ trợ bởi một số kỹ thuật sau đây:
Bộ sinh các lựa chọn theo kinh nghiệm (Heuristic Choice Generators): có nghĩa là tập các lựa chọn trong một trạng thái nào đó không cần phải hoàn thành. Kiểu động không thể sinh ra tất cả các giá trị có thể xảy ra nhưng về mặt kiểm tra hành vi hệ thống nó có thể đủ để thử chỉ ba lựa chọn: nhỏ hơn, bằng và lớn hơn ngưỡng. Khả năng quan trọng là để các kinh nghiệm này có thể cấu hình được cốt để chúng có thể dễ dàng được mở rộng hay được thích nghi với các nhu cầu của ứng dụng cụ thể.
Giảm thứ tự bộ phận (Partial Order Reduction): là kỹ thuật quan trọng nhất để giảm không gian trạng thái trong các chương trình tương tranh. Mục đích chỉ để xem xét các chuyển ngữ cảnh tại các hoạt động có ảnh hưởng ngang qua các biên tiến trình như các lệnh PUTFIELD trên các đối tượng có thể truy cập từ các luồng khác nhau. Sự làm giảm thứ tự bộ phận của JPF lợi dụng các mã Java bytecode và các thông tin có khả năng với tới đạt được từ trình thu thập rác.
Sự thực thi của máy ảo chủ (Host VM Execution): JPF là một máy ảo Java được viết bằng Java, tức là nó chạy trên đỉnh của một máy ảo chủ (host VM). Đối với các thành phần không phải là các đặc tính hợp lệ, nó hiểu là để giao phó sự thực thi từ JPF được theo dõi theo trạng thái cho máy ảo chủ được theo dõi không trạng thái. Kỹ thuật MJI tương ứng đặc biệt phù hợp để xử lý sự mô phỏng vào ra và chức năng thư viện chuẩn khác.
Sự trừu tượng hóa trạng thái (State Abstraction): JPF lưu trữ tất cả các thay đổi của bộ nhớ heap, ngăn xếp và tiến trình mà đôi khi có sự quá tải lớn nếu nó đi đến việc quyết định lựa chọn thực thi hai trạng thái khác nhau theo ngữ cảnh của một ứng dụng nào đó. Ví dụ, việc so khớp trạng thái được dựa trên sự phân tích hình thù của các cấu trúc dữ liệu có thể mang lại sự giảm trạng thái đáng kể và đã được sử dụng thành công trong các ứng dụng JPF gần đây.
3) Việc làm giảm chi phí lưu trữ trạng thái có liên quan chỉ yếu đến các đặc trưng cài đặt lõi JPF. Nó không phải là thước đo chính để giải quyết sự bùng nổ không gian trạng thái nhưng việc lưu trữ trạng thái hiệu quả có tính chất bắt buộc đối với một công cụ kiểm chứng mô hình phần mềm. Vì các chuyển trạng thái thường dẫn đến một số lượng nhỏ các thay đổi, JPF sử dụng một kỹ thuật được gọi là sụt trạng thái (state collapsing) để làm giảm các yêu cầu về bộ nhớ mỗi trạng thái bằng việc lưu trữ các chỉ số trong các bể chứa thành phần trạng thái riêng (hash table) thay vì việc lưu trữ các giá trị thay đổi một cách trực tiếp.
2.3. Các đề án mở rộng của 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. Đề án mở rộng nổi bật nhất là thực thi ký hiệu (symbolic execution) để sinh ra các ca kiểm thử. Thực thi ký hiệu sẽ được trình bày chi tiết và được ứng dụng để sinh các ca kiểm thử trong chương 4.
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): đây là một ví dụ về kiểm chứng mô hình một lớp cụ thể của ứng dụng Java – các chương trình Swing và AWT. Mở rộng này được thực hiện như là một tập của MJI (Model Java Interface) được mô hình hóa bởi các thư viện chuẩn thay thế về mặt chức năng của các thư viện javax.swing.* and java.awt.* để các ứng dụng giao diện người dùng Java chuẩn có thể được kiểm thử đối với các lựa chọn đầu vào khác nhau của người sử dụng.
Sinh dữ liệu kiểm thử ký hiệu (Symbolic Test Data Generation): phần mở rộng thực thi ký hiệu này sử dụng BytecodeFactory để nạp chồng các mã bytecode lõi của JPF để tạo ra các ca kiểm thử cụ thể. Công việc này được thực hiện bằng việc sử dụng hệ thống thuộc tính trường (field)/cấu trúc ngăn xếp (stackframe) của JPF để thu thập các điều kiện dẫn ký hiệu sau đó đưa chúng vào một bộ giải quyết giàng buộc nhằm thu được dữ liệu kiểm thử cụ thể.
Sinh kiểm thử luồng an toàn ký hiệu (Symbolic Threadsafety Test Generation): đây là một chế độ thực thi ký hiệu rất đơn giản cố gắng nhận dạng các vấn đề an toàn luồng tiềm tàng và sau đó sinh mã để kiểm thử điều này với chế độ thực thi JPF giá trị cụ thể. Thông tin ký hiệu đơn giản hơn nhiều so với phần mở rộng thực thi ký hiệu trên và có thể được giữ lại trong vết thực thi. Danh mục kiểm thử được thực hiện đầu tiên phát hiện sự truy cập không đồng bộ của các trường từ bên trong cùng phương thức công cộng, để nhận dạng các ứng viên cho bộ nghe PreciseRaceDetector.
Khung làm việc kiểm chứng cấu thành (Compositional Verification Framework): phần mở rộng này thực hiện một giải thuật học máy có thể được sử dụng cho việc suy luận đảm bảo giả định, để phân hoạch một hệ thống thành các thành phần có thể kiểm chứng riêng biệt. Mục đích của thực thi này là để cải thiện đáng kể hiệu năng của JPF. Nó cũng có thể được sử dụng để sinh ra các môi trường giả định cho kiểm chứng mô hình UML, để xác định các chuỗi sự kiện đúng đắn.
Kiểm chứng đặc tính số (Numeric Property Verification): sự mở rộng thay thế mã bytecode này được khởi đầu như là một tập các lớp chỉ thị số để phát hiện sự tràn số và nó cũng bao gồm sự truyền giá trị không chính xác (NaN, Inf), sự so sánh chính
xác số dấu phảy động và sự hủy bỏ thê thảm tiềm tàng (thiếu chính xác bởi phép trừ/cộng).
Kiểm chứng mô hình biểu đồ trạng thái UML (UML State Chart Model Checking): khung làm việc biểu đồ trạng thái là một biến thể của kiểm chứng mô hình