|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
This article is in the Product Showcase section for our sponsors at The Code Project. These reviews are intended to provide you with information on products and services that we consider useful and of value to developers.
IntroductionThere are three main types of software bugs:
Building a robust regression suite is the best way to identify poorly implemented requirements, and performing negative testing is the best way to identify confused user errors. However, finding missing requirements is difficult because it’s not clear what you are looking for. Flow analysis, which basically analyzes paths through the code without executing it, is the only known automated testing technique that leads you to such problems. For instance, assume that flow analysis identified a In addition to pointing to missing requirements, flow analysis also can expose construction problems and logical flaws in the application. It is designed to be used as part of a comprehensive regression test suite, which also includes pattern-matching static analysis, unit tests, HttpUnit tests, in-container tests, module tests, and API tests, and any other tests you use to verify the software. Running the complete regression test suite — including flow analysis and all the other tests — automatically and regularly (e.g., every 24 hours) is the most effective way to determine if code modifications/additions introduced new problems, broke existing functionality, or caused unexpected side effects. This paper examines why and how to add flow analysis to your existing testing strategies. After introducing the general concept and benefits of flow analysis, it explains how flow analysis can be performed using Parasoft BugDetective™ technology, and demonstrates how it can be applied to bolster both your static analysis and unit testing efforts. Static Code Flow Analysis - BackgroundThe term static code analysis means different things to different people in the software industry. There seems to be two main static analysis approaches: (1) program execution or flow-based analysis and (2) pattern-based analysis. For program execution adherents, static analysis means trying to logically execute the program — sometimes symbolically — to uncover code problems such as memory corruption, leaks, and exceptions. This type of testing largely focuses on identifying code problems without creating test cases. It provides developers with the "instant feedback" they need to quickly address defects and security vulnerabilities on the desktop — while they are still working on the code and it is fresh in their minds — and it prevents defects and vulnerabilities from making their way further downstream in the software development process, which is where they are much more expensive to identify and remediate. Parasoft Static Analysis and BugDetective TechnologyParasoft’s static analysis technologies support both flow-based static analysis and pattern-based static analysis. Parasoft’s flow-based static analysis technology, called BugDetective, provides effortless early detection of runtime problems and application instabilities (such as By automatically tracing and simulating execution paths through even the most complex applications — those with paths that span multiple methods, classes, and/or packages and contain dozens of sequence calls — BugDetective exposes defects that would be very difficult and time-consuming to find through manual testing or inspections, and would be exponentially more costly to fix if they were not detected until runtime. Using BugDetective, developers can find, diagnose, and fix classes of software errors that can evade pattern-based static analysis and/or unit testing. Exposing these defects early in the software development lifecycle saves hours of diagnosis and potential rework. BugDetective static analysis has two applications within Parasoft Jtest:
Benefits of Using BugDetectiveUsing BugDetective, development teams gain the following key benefits:
public int strlen(String str)
{
return str.length();
}
In The Trenches with BugDetectiveBugDetective’s unique breed of static analysis determines whether an application’s execution paths match “suspicious behavior” profiles, which are implemented as rules. For each defect found, a hierarchical flow path details the complete execution path that leads to the identified defect, ending with the exact line of code where the bug manifests itself. To reduce the time and effort required to diagnose and correct each problem found, flow path details are supplemented with extensive annotations (for example, a To make the analysis process more flexible and tailored to your unique project needs, some rules can be parameterized. As a result, BugDetective can even be used to detect violations bound to usage of very specific APIs. Understanding Flow PathsIn the Jtest GUI, each BugDetective violation is represented by a hierarchical flow path that precisely describes the code that leads to the identified problem. Each element in the path is a line of code that is executed during runtime. If a flow path has a call to a method, the element representing that method call is a node whose sub-nodes represent execution flow within the called method. The final element in the execution path is always the point where the bug manifests itself. The complete path is presented in order to explain why there is a bug at the final point.
Flow path elements are marked with icons that help explain exception handling behavior. If a path has a call to a method that happens to throw an exception on that path, the path element corresponding to the method call is marked by a red sphere. This red sphere indicates that the flow proceeds to a catch or finally block instead of proceeding as normal. Each element in the flow path has a tool tip that describes the variables related to the violation. For example, a
If you want to navigate through the code related to a reported execution path, use the Next Violation Element and Previous Violation Element buttons in the Jtest view toolbar. Understanding and Accessing the Violation Origin and Violation PointThe violation itself is represented by an execution path with two marked points:
You can easily access the violation origin and violation point by right-clicking a reported violation (the node with the yellow caution icon) and then choosing the appropriate command from the shortcut menu (either Show Violation Origin or Show Violation Point). For example, a "Null pointer exception" rule violation has the commands Show Violation Origin (Point of Null Assignment) and Show Violation Point (NullPointerException Point) in order to help you understand why the exception may occur in the code.
Running BugDetective Static Flow AnalysisIn its primary application, BugDetective can be used as a part of Jtest static analysis to statically simulate execution paths through an application and to look for vulnerabilities by analyzing these paths. The depth of the analysis can be decreased for a faster analysis and can be increased for a more thorough and in-depth analysis. To better understand the types of defects that BugDetective flow analysis can expose, consider how Jtest’s BugDetective analysis can be applied to sample Java classes. One sample class involves a class instance field that can be null (Example 1 – Both of the examples contain instance field and local variable variations of the same defects. The methods named BugDetective flags the following defects in the two sample files:
X indicates that Jtest BugDetective did not report a violation in the method and √ indicates that Jtest did report a violation in that method. Example 1public class TestFields {
Object x;
TestFields(Object x) {
this.x = x;
}
int falsePositive1(int level) {
x = null;
if (level > 0)
x = new Object();
if (level > 4)
return x.hashCode();
return 0;
}
int truePositive1(int level) {
x = null;
if (level > 0)
x = new Object();
if (level < 4)
return x.hashCode();
return 0;
}
int falsePositive2(boolean b) {
x = null;
if (b)
x = new Object();
if (b)
return x.hashCode();
return 0;
}
int truePositive2(boolean b) {
x = null;
if (b)
x = new Object();
if (!b)
return x.hashCode();
return 0;
}
int falsePositive3(boolean b) {
Object y = null;
if (x != null)
y = new Object();
if (y != null)
return x.hashCode() + y.hashCode();
else
return 0;
}
int truePositive3(boolean b) {
Object y = null;
if (x != null)
y = new Object();
if (y != null)
return x.hashCode() + y.hashCode();
else
return x.hashCode();
}
int falsePositive4(boolean a, boolean b) {
x = null;
Object y = null;
if (a) x = "x";
if (b) y = "y";
if (y != null)
return x.hashCode() + y.hashCode();
else
return 0;
}
int truePositive4(boolean a, boolean b) {
x = null;
Object y = null;
if (a) x = "x";
if (b) y = "y";
if (y != null)
return x.hashCode() + y.hashCode();
else
return x.hashCode();
}
int truePositive5() {
if (x == null) return x.hashCode();
return 0;
}
int truePositive6() {
if (x == null) {
Object y = x;
return y.hashCode();
}
return 0;
}
int ifalsePositive1(boolean b) {
x = null;
if (!b)x = new Object();
return LocalHelper.helper1(x, b);
}
int itruePositive1(boolean b) {
x = null;
if (b) x = new Object();
return LocalHelper.helper1(x, b);
}
int itruePositive2() {
x = null;
return LocalHelper.helper2(x);
}
int itruePositive3(boolean b) {
x = null;
if (b) x = "x";
return LocalHelper.helper3(x);
}
}
Example 2public class TestLocal {
int falsePositive1(int level) {
Object x = null;
if (level > 0)
x = new Object();
if (level > 4)
return x.hashCode();
return 0;
}
int truePositive1(int level) {
Object x = null;
if (level > 0)
x = new Object();
if (level < 4)
return x.hashCode();
return 0;
}
int falsePositive2(boolean b) {
Object x = null;
if (b)
x = new Object();
if (b)
return x.hashCode();
return 0;
}
int truePositive2(boolean b) {
Object x = null;
if (b)
x = new Object();
if (!b)
return x.hashCode();
return 0;
}
int falsePositive3(Object x, boolean b) {
Object y = null;
if (x != null)
y = new Object();
if (y != null)
return x.hashCode() + y.hashCode();
else
return 0;
}
int truePositive3(Object x, boolean b) {
Object y = null;
if (x != null)
y = new Object();
if (y != null)
return x.hashCode() + y.hashCode();
else
return x.hashCode();
}
int falsePositive4(boolean a, boolean b) {
Object x = null;
Object y = null;
if (a) x = "x";
if (b) y = "y";
if (y != null)
return x.hashCode() + y.hashCode();
else
return 0;
}
int truePositive4(boolean a, boolean b) {
Object x = null;
Object y = null;
if (a) x = "x";
if (b) y = "y";
if (y != null)
return x.hashCode() + y.hashCode();
else
return x.hashCode();
}
int truePositive5(Object x) {
if (x == null) {
return x.hashCode();
}
return 0;
}
int truePositive6(Object x) {
if (x == null) {
Object y = x;
return y.hashCode();
}
return 0;
}
int ifalsePositive1(boolean b) {
Object x = null;
if (!b) x = new Object();
return LocalHelper.helper1(x, b);
}
int itruePositive1(boolean b) {
Object x = null;
if (b) x = new Object();
return LocalHelper.helper1(x, b);
}
int itruePositive2() {
return LocalHelper.helper2(null);
}
int itruePositive3(boolean b) {
Object x = null;
if (b) x = "x";
return LocalHelper.helper3(x);
}
}
public class LocalHelper {
// Bug when x is null and b is false
public static int helper1(Object x, boolean b) {
if (b) return 0;
return x.hashCode();
}
public static int helper2(Object x) {
return x.hashCode();
}
public static int helper3(Object x) {
return x.hashCode();
}
}
Taking a closer look at the results, notice that BugDetective flagged no false positives in these examples. As Parasoft was developing BugDetective, one of our main goals was to ensure that the level of noise (with respect to reporting of false positives) was minima l— even if this meant that fewer defects would be reported. In this specific case, all ten of the false positives were not reported; this is a very good result, and it shows how this design decision manifests itself in BugDetective. Along those lines, BugDetective considers the defects in the Consider the following code from the Object x; //NPE origin
TestFields(Object x) {
this.x = x;
}
int truePositive3(boolean b) {
Object y = null;
if (x != null)
y = new Object();
if (y != null)
return x.hashCode() + y.hashCode();
else
return x.hashCode(); //NPE
}
The instance variable Jtest BugDetective does not flag this violation because when simulating execution paths through the code, it sees a potential violation point on the path (the line marked with TestFields tf = new TestFields();
tf.truePositive3(true|false);
Nor did it find a path such as this: TestFields tf = new TestFields(null);
tf.truePositive3(true|false);
However, assume that the following method is added to the void callerTruePositive3() {
TestFields tf = new TestFields(null);
tf.truePositive3(true);
}
Jtest BugDetective now flags this violation since it sees the violation origin and violation point, as well as a code path that leads from one to the other. If the Moreover, even though BugDetective does not flag this violation by default, Jtest’s unit testing will report this defect as a unit testing defect; code similar to that in the Using BugDetective in Cooperation with Jtest Unit TestingBugDetective can also be used to check whether reported exceptions could actually be triggered by real application paths. If a reported exception is validated by BugDetective, its severity level is elevated, and it is specially marked in the Jtest view. This helps users determine whether reported exceptions are “real defects.” If you configure Jtest to validate exceptions with BugDetective, Jtest will automatically collect the runtime exceptions reported from test case execution, then use BugDetective to try to determine if they can really occur when the code is executed. For example, assume that you have an application A which has a module
With exception validation enabled, Jtest can distinguish between these two categories of reported exceptions. This way, you can instantly tell if a reported exception is a real defect— without having to ana¬lyze the code. The value of the cooperation between BugDetective and Jtest unit testing is highlighted in the following four scenarios:
BugDetective finds violation paths that it cannot determine whether to report (according to its heuristics), but then these defects are reported because unit testing flags the same paths. Unit testing exceptions that were validated with BugDetective will be marked with a red shadow in the Jtest view, and their severity level will also be elevated by the degree specified in the Test Configuration’s Execution> Severities tab. This allows customers to configure reporting of exceptions to suit their own environment and applications.
Unit testing exceptions validated by BugDetective point to actual defects, and should be corrected. Exceptions that were checked with BugDetective, but not validated, indicate that the class is not designed to handle the data that was supplied by the test case and should not be passed that data. Contracts should be added to specify that such data is not permitted. BugDetective Promotes Exceptions Reported by Unit TestingExample 3public class SimpleNeverHappenedAndNotAcknowledgeByBD {
private class Name {
String _name;
Name (String name) {
_name = name;
}
public String toString() {
return _name;
}
}
Name _objectName;
public String getName () {
return _objectName.toString();
}
void initialize (String name) {
_objectName = new Name (name);
}
/**
public static void main(String[] args) {
SimpleNeverHappenedAndNotAcknowledgeByBD obj =
new SimpleNeverHappenedAndNotAcknowledgeByBD();
System.out.println(obj.getName()); //NPE
} */
}
Example 3 demonstrates how exceptions reported by unit testing are promoted by BugDetective. First, we ran the built-in “BugDetective” Test Configuration on Example 3, then we ran the built-in “Generate and Execute Unit Tests” Test Configuration, then we ran a user-defined “Generate and Execute Unit Tests” Test Configuration that had BugDetective validation enabled. With the code in the When unit testing is performed with the built-in “Generate and Execute Unit Tests” Test Configuration, Jtest reports one public void testGetName5() throws Throwable {
SimpleNeverHappenedAndNotAcknowledgeByBD testedObject =
new SimpleNeverHappenedAndNotAcknowledgeByBD();
String result = testedObject.getName();
// NullPointerException thrown
// at example.A.SimpleNeverHappenedAndNotAcknowledgeByBD.getName
(SimpleNeverHappenedAndNotAcknowledgeByBD.java:36)
// jtest_unverified
}
When unit testing is performed with the user-defined “Generate and Execute Unit Tests” Test Configuration with BugDetective validation enabled, Jtest reports exactly the same results. In this case, that is expected. Since BugDetective found no violation paths in this example, no unit testing exceptions could be promoted. Now, let’s repeat the same test with the public void testMain1() throws Throwable {
String[] strings = new String[] {};
SimpleNeverHappenedAndNotAcknowledgeByBD.main(strings);
// NullPointerException thrown
// at example.A.SimpleNeverHappenedAndNotAcknowledgeByBD.getName(
// SimpleNeverHappenedAndNotAcknowledgeByBD.java:36)
// at example.A.SimpleNeverHappenedAndNotAcknowledgeByBD.main(
// SimpleNeverHappenedAndNotAcknowledgeByBD.java:50)
// jtest_unverified
}
Running unit testing with the user-defined “Generate and Execute Unit Tests” Test Configuration with BugDetective validation enabled indeed increases the severity of both unit test cases that were reporting Unit Testing and BugDetective in Cooperation Filter Out Exceptions Reported by BugDetectiveExample 4public class BDBogusNotApprovedByUT {
private boolean B = true;
BDBogusNotApprovedByUT() {
B = false;
}
public void test() {
Object o = null;
if (B) {
o.toString(); //NPE
}
else {
// we should go into this method (violation search method analysis guide)
// CallFromAnotherFile.methodWithNpe(o);
}
}
}
One could argue about the value of BugDetective filtering, since BugDetective was already catching this BugDetective triggers a warning on line 17, which is marked with Unit testing on its own does not report any exceptions, and hence unit testing with BugDetective validation enabled does not report any exceptions either. Therefore, even though BugDetective by itself reports this false positive, it is filtered out when BugDetective works in cooperation with unit testing. This is a very simple example and one can claim that such a case should be fixed. Perhaps — but there are many possibilities where BugDetective’s heuristics can make a mistake and report a false positive. In such cases, unit testing can successfully be used to either validate or filter out these exceptions. ConclusionThe unique breed of flow analysis that BugDetective provides helps software development teams find critical runtime bugs without executing code, as well as validate whether exceptions exposed by unit test cases are “real bugs” that could actually surface in the field. BugDetective exposes bugs that would often evade pattern-matching static analysis and unit testing, yet would be very difficult and time-consuming to find through manual testing or inspections When BugDetective is applied as part of a comprehensive regression test suite that also includes pattern-matching static analysis, unit testing, in-container testing (for Java), API testing, module testing, and so forth, it helps development teams to:
Parasoft CorporationFor 20 years, Parasoft has investigated how and why software errors are introduced into applications. Our solutions leverage this research to deliver quality as a continuous process throughout the SDLC. This promotes strong code foundations, solid functional components, and robust business processes. Whether you are delivering Service-Oriented Architectures (SOA), evolving legacy systems, or improving quality processes—draw on our expertise and award-winning products to increase productivity and the quality of your business applications. For more information, visit the Parasoft homepage.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||