COSC215: Software Analysis and Testing

Intro to SAT

Published:

Software Engineering

Common job descriptions for someone who writes code are programmer, software developer, or software engineer. They are all describing the same job. The variety in names hints at the similarities and differences between software engineering and other engineering. Engineering aims to build something that serves a function, a product, or an artifact.

Most engineering activities involve a design that is separately developed from the product. There are three inter-related main ways in which software engineering differs:

  1. It is malleable (the product can be changed without necessarily changing the design)
  2. It is human-intensive (in regular engineering, the product development involves manufacturing, most often mechanized with little to no involvement from humans)
  3. The quality of the product is more often than not intermixed with that of the design. The right process helps to produce the right product and vice versa.

In fact, it’s more common to hear about the software process vs. the software product in software engineering than design vs. product.

Software Quality

Software Analysis and Testing are very closely related to Software Quality. There are many desirable software qualities in software engineering: correctness, reliability (relative correctness), robustness (reasonable behavior even in circumstances that were not anticipated in the requirements), performance, usability (ease of use), verifiability (we will cover testing in this course), security, maintainability, reusability, portability, understandability (although some applications are less clear than others the design and implementation should not further affect it), interoperability, productivity, timeliness, visibility (transparency of the process, amenable for external examination).

When addressing software quality it’s also useful to consider the different parties: users, developers, and managers because some of the software qualities are visible to the user (we call these external qualities) and some are only visible to the developers of the system (we call these internal qualities). A user wants a product to be reliable, efficient, and easy to use. The developer wants it to be verifiable, maintainable, portable, and extensible, the manager wants the process to be productive, predictable, and easy to control.

Furthermore, while the process is for the most part not visible to the user, the product has user-visible components such as executable code and user manual, and intermediate components (work products or artifacts) such as requirements, design documents, test data, etc. These intermediate components can also be visible to other types of users (other than the end-users).

We conclude that there is a high overlap between external and internal qualities, product and process qualities which makes it hard to standardize it. There are several software quality models that share the same essential aspects but differ in emphasis and organization.

Software Quality Principles

Some general principles to achieve the software qualities discussed prior are:

  • Rigor and Formality. Rigor means precision and exactness. Formality is a stronger requirement than rigor which requires the use of mathematical laws to evaluate and proceed. It’s impossible to achieve formality in software engineering because of its creative nature so the best we can strive for in many instances is rigor. We will briefly study some formal methods in this course. However, one crucial expectation of the course is that you will practice rigor in your software engineering activities. For example, many students coming into this course are very intuitive about their testing practices. In a few weeks, we will discuss the differences between intuitive and systematic testing. We will then learn different systematic and effective methods to engage in testing. Note that although in the past you were assessed based on the end product (the tests you provided), in this course you will be primarily assessed on the rigor of the process and the reporting.
  • Separations of concerns
  • Modularity
  • Abstraction
  • Incrementality
  • Anticipation of change and generality

Code Style

Code style is connected to many desirable software qualities of code, including correctness. Let’s look at this code:

class BoardComputer {

    CruiseControl cruiseControl;

    void authorize(User user) {
        Objects.requireNonNull(user);
        if (user.isUnknown())
            cruiseControl.logUnauthorizedAccessAttempt();
        if (user.isAstronaut())
            cruiseControl.grantAccess(user);
        if (user.isCommander())
            cruiseControl.grantAccess(user);
            cruiseControl.grantAdminAccess(user);
    }
}

Note that the last line of code is not part of the body of the last if statement and in fact, gets executed every time the method authorize is invoked. For this particular application, this goes beyond correctness and presents a high security risk. The fix is to add curly braces around the body like so:

class BoardComputer {

    CruiseControl cruiseControl;

    void authorize(User user) {
        Objects.requireNonNull(user);
        if (user.isUnknown()){
            cruiseControl.logUnauthorizedAccessAttempt();
        }
        if (user.isAstronaut()){
            cruiseControl.grantAccess(user);
        }
        if (user.isCommander()){
            cruiseControl.grantAccess(user);
            cruiseControl.grantAdminAccess(user);
        }
    }
}

One may also conclude that the three cases are mutually exclusive and combine them in an if-else-if structure like so:

class BoardComputer {

    CruiseControl cruiseControl;

    void authorize(User user) {
        Objects.requireNonNull(user);
        if (user.isUnknown()){
            cruiseControl.logUnauthorizedAccessAttempt();
        }
        else if (user.isAstronaut()){
            cruiseControl.grantAccess(user);
        }
        else if (user.isCommander()){
            cruiseControl.grantAccess(user);
            cruiseControl.grantAdminAccess(user);
        }
    }
}

However, note that there is an asymmetry in the code from the fact that we are mixing authorizing code with non-authorizing code. We can increase the understandability of our code by separating them out like so:

class BoardComputer {

    CruiseControl cruiseControl;

    void authorize(User user) {
        Objects.requireNonNull(user);
        if (user.isUnknown()) {
            cruiseControl.logUnauthorizedAccessAttempt();
            return;
        }

        if (user.isAstronaut()) {
            cruiseControl.grantAccess(user);
        } else if (user.isCommander()) {
            cruiseControl.grantAccess(user);
            cruiseControl.grantAdminAccess(user);
        }
    }
} 

[Code Style Worksheet] [Answers]

Appropriate constructs

Published:

One of the examples from last class used an enum structure:

private Result analyzeOrganic(Sample sample) {
    if (microscope.isHumanoid(sample) == false) {
        return Result.ALIEN;
    } else {
        return Result.HUMANOID;
    }
}

An enumeration is a pre-defined set of values, for example:

public enum Result {
    HUMANOID, INORGANIC, ALIEN
}

The advantage to using an enumeration is that when you declare a variable, a parameter or return type of an enumeration type then all the values (stored in the variable, passed to the parameter or returned) must be in the predefined set of the enumeration. For example, analyzeOrganic can only return three values.

Additionally, you can add attributes and constructor to enumerations just like with classes likes so:

public enum Result {
    HUMANOID(75), 
    INORGANIC(40), 
    ALIEN(100);

    final int coolnessIndex;

    Result(int newCI){
        this.coolnessIndex = newCI;
    }
}

Warm-up Problem: How can we improve the quality of this code snippet:

class CruiseControl {

    private double targetSpeedKmh;

    void setPreset(int speedPreset) {
        if (speedPreset == 2) {
            setTargetSpeedKmh(16944);
        } else if (speedPreset == 1) {
            setTargetSpeedKmh(7667);
        } else if (speedPreset == 0) {
            setTargetSpeedKmh(0);
        }
    }

    void setTargetSpeedKmh(double speed) {
        targetSpeedKmh = speed;
    }
}

Numbers without an apparent meaning that steer the program are called magic numbers. We can dispel their magic and thus improve the quality of the code by making them constants:

class CruiseControl {

    static final int STOP_PRESET = 0;
    static final int PLANETARY_SPEED_PRESET = 1;
    static final int CRUISE_SPEED_PRESET = 2;

    static final double CRUISE_SPEED_KMH = 16944;
    static final double PLANETARY_SPEED_KMH = 7667;
    static final double STOP_SPEED_KMH = 0;

   private double targetSpeedKmh;

    void setPreset(int speedPreset) {
        if (speedPreset == CRUISE_SPEED_PRESET) {
            setTargetSpeedKmh(CRUISE_SPEED_KMH);
        } else if (speedPreset == PLANETARY_SPEED_PRESET) {
            setTargetSpeedKmh(PLANETARY_SPEED_KMH);
        } else if (speedPreset == STOP_PRESET) {
            setTargetSpeedKmh(STOP_SPEED_KMH);
        }
    }

    void setTargetSpeedKmh(double speed) {
        targetSpeedKmh = speed;
    }
}

Can we do better?

Note that nothing restricts the user to call setPreset with values other than the three values it checks for and the function terminates without error. We can make the semantics of this function more transperant for outside usage by using an enum construct like so:

class CruiseControl {

    private double targetSpeedKmh;

    void setPreset(SpeedPreset speedPreset) {
        Objects.requireNonNull(speedPreset);

        setTargetSpeedKmh(speedPreset.speedKmh);
    }

    void setTargetSpeedKmh(double speedKmh) {
        targetSpeedKmh = speedKmh;
    }
}
enum SpeedPreset {
    STOP(0), PLANETARY_SPEED(7667), CRUISE_SPEED(16944);

    final double speedKmh;

    SpeedPreset(double speedKmh) {
        this.speedKmh = speedKmh;
    }
}

[Appropriate Construct Worksheet] [Answers]

Conventions

Published:

Conventions are rules that are widely agreed upon by professionals. Used approapriately and consistently, they can help with the readability of the code.

Rule 1: Remove excessive comments for example:

class Inventory {
    // Fields (we only have one)
    List<Supply> supplies = new ArrayList<>(); // The list of supplies.

    // Methods
    int countContaminatedSupplies() {
        // TODO: check if field is already initialized (not null)

        int contaminatedCounter = 0; // the counter
        // No supplies => no contamination
        for (Supply supply : supplies) { // begin FOR
            if (supply.isContaminated()) {
                contaminatedCounter++; // increment counter!
            } // End IF supply is contaminated
        }// End FOR

        // Returns the number of contaminated supplies.
        return contaminatedCounter; // Handle with care!
    }
} // End of Inventory class

Becomes:

class Inventory {

    List<Supply> supplies = new ArrayList<>();

    int countContaminatedSupplies() {
        if (supplies == null || supplies.isEmpty()) {
            // No supplies => no contamination
            return 0;
        }

        int contaminatedCounter = 0;
        for (Supply supply : supplies) {
            if (supply.isContaminated()) {
                contaminatedCounter++;
            }
        }

        return contaminatedCounter;
    }
}

Rule 2: Remove code in comments for example:

class LaunchChecklist {

    List<String> checks = Arrays.asList(
            "Cabin Leak",
            // "Communication", // Do we actually want to talk to Houston?
            "Engine",
            "Hull",
            // "Rover", // We won't need it, I think...
            "OxygenTank"
            //"Supplies"
    );

    Status prepareLaunch(Commander commander) {
        for (String check : checks) {
            boolean shouldAbortTakeoff = commander.isFailing(check);
            if (shouldAbortTakeoff) {
                //System.out.println("REASON FOR ABORT: " + item);
                return Status.ABORT_TAKE_OFF;
            }
        }
        return Status.READY_FOR_TAKE_OFF;
    }
}

Becomes:

class LaunchChecklist {

    List<String> checks = Arrays.asList(
            "Cabin Leak",
            "Engine",
            "Hull",
            "OxygenTank"
    );

    Status prepareLaunch(Commander commander) {
        for (String check : checks) {
            boolean shouldAbortTakeoff = commander.isFailing(check);
            if (shouldAbortTakeoff) {
                return Status.ABORT_TAKE_OFF;
            }
        }
        return Status.READY_FOR_TAKE_OFF;
    }
}

Rule 3: Replace comments with constants for example:

enum SmallDistanceUnit {

    CENTIMETER,
    INCH;

    double getConversionRate(SmallDistanceUnit unit) {
        if (this == unit) {
            return 1; // identity conversion rate
        }

        if (this == CENTIMETER && unit == INCH) {
            return 0.393701; // one centimeter in inch
        } else {
            return 2.54; // one inch in centimeters
        }
    }
}

Becomes:

enum SmallDistanceUnit {

    CENTIMETER,
    INCH;

    static final double INCH_IN_CENTIMETERS = 2.54;
    static final double CENTIMETER_IN_INCHES = 1 / INCH_IN_CENTIMETERS;
    static final int IDENTITY = 1;


    double getConversionRate(SmallDistanceUnit unit) {
        if (this == unit) {
            return IDENTITY;
        }

        if (this == CENTIMETER && unit == INCH) {
            return CENTIMETER_IN_INCHES;
        } else {
            return INCH_IN_CENTIMETERS;
        }
    }
}

Rule 4: Replace comments with utility methods for example:

class FuelSystem {

    List<Double> tanks = new ArrayList<>();

    int getAverageTankFillingPercent() {
        double sum = 0;
        for (double tankFilling : tanks) {
            sum += tankFilling;
        }
        double averageFuel = sum / tanks.size();
        // round to integer percent
        return Math.toIntExact(Math.round(averageFuel * 100));
    }
}

Becomes:

class FuelSystem {

    List<Double> tanks = new ArrayList<>();

    int getAverageTankFillingPercent() {
        double sum = 0;
        for (double tankFilling : tanks) {
            sum += tankFilling;
        }
        double averageFuel = sum / tanks.size();
        return roundToIntegerPercent(averageFuel);
    }

    static int roundToIntegerPercent(double value) {
        return Math.toIntExact(Math.round(value * 100));
    }
}

Rule 5: Do document implementation decisions using a pattern like this:

/*
* In the context of [USE CASE],
* facing [CONCERN]
* we decided for [OPTION]
* to achieve [QUALITY],
* accepting [DOWNSIDE].
*/

For example:

class Inventory {
    
    private List<Supply> list = new ArrayList<>();

    void add(Supply supply) {
        list.add(supply);
        Collections.sort(list);
    }

    boolean isInStock(String name) {
        // fast implementation
        return Collections.binarySearch(list, new Supply(name)) != -1;
    }
}

Becomes:

    boolean isInStock(String name) {
        /*
         * In the context of checking availability of supplies by name,
         * facing severe performance issues with >1000 supplies
         * we decided to use the binary search algorithm
         * to achieve item retrieval within 1 second,
         * accepting that we must keep the supplies sorted.
         */
        return Collections.binarySearch(list, new Supply(name)) != -1;
    }

Rule 6: Use examples in your documentation for instance:

class Supply {

    /**
     * The code universally identifies a supply.
     *
     * It follows a strict format, beginning with an S (for supply), followed
     * by a five digit inventory number. Next comes a backslash that
     * separates the country code from the preceding inventory number. This
     * country code must be exactly two capital letters standing for one of
     * the participating nations (US, EU, RU, CN). After that follows a dot
     * and the actual name of the supply in lowercase letters.
     */
    static final Pattern CODE =
            Pattern.compile("^S\\d{5}\\\\(US|EU|RU|CN)\\.[a-z]+$");
}

Becomes:

class Supply {

    /**
     * The expression universally identifies a supply code.
     *
     * Format: "S<inventory-number>\<COUNTRY-CODE>.<name>"
     *
     * Valid examples: "S12345\US.pasta", "S08342\CN.wrench",
     * "S88888\EU.laptop", "S12233\RU.brush"
     *
     * Invalid examples:
     * "R12345\RU.fuel"      (Resource, not supply)
     * "S1234\US.light"      (Need five digits)
     * "S01234\AI.coconut"   (Wrong country code. Use US, EU, RU, or CN)
     * " S88888\EU.laptop "  (Trailing whitespaces)
    */
    static final Pattern SUPPLY_CODE =
            Pattern.compile("^S\\d{5}\\\\(US|EU|RU|CN)\\.[a-z]+$");
}

[Naming Conventions Worksheet] [Answers]

How to address things going wrong

Published:

  1. Check for the type before casting, for example:
class Network {

    ObjectInputStream inputStream;
    InterCom interCom;

    void listen() throws IOException, ClassNotFoundException {
        while (true) {
            Object signal = inputStream.readObject();
            CrewMessage crewMessage = (CrewMessage) signal;
            interCom.broadcast(crewMessage);
        }
    }
}

With this improvement becomes:

class Network {

    ObjectInputStream inputStream;
    InterCom interCom;

    void listen() throws IOException, ClassNotFoundException {
        while (true) {
            Object signal = inputStream.readObject();
            if (signal instanceof CrewMessage) {
                CrewMessage crewMessage = (CrewMessage) signal;
                interCom.broadcast(crewMessage);
            }
        }
    }
}

Exceptions

  1. Fail Fast: Separate normal paths from validation paths, for example:
class CruiseControl {
    static final double SPEED_OF_LIGHT_KMH = 1079252850;
    static final double SPEED_LIMIT = SPEED_OF_LIGHT_KMH;

    private double targetSpeedKmh;

    void setTargetSpeedKmh(double speedKmh) {
        if (speedKmh < 0) {
            throw new IllegalArgumentException();
        } else if (speedKmh <= SPEED_LIMIT) {
            targetSpeedKmh = speedKmh;
        } else {
            throw new IllegalArgumentException();
        }
    }
}

Becomes:

class CruiseControl {
    static final double SPEED_OF_LIGHT_KMH = 1079252850;
    static final double SPEED_LIMIT = SPEED_OF_LIGHT_KMH;

    private double targetSpeedKmh;

    void setTargetSpeedKmh(double speedKmh) {
        if (speedKmh < 0 || speedKmh > SPEED_LIMIT) {
            throw new IllegalArgumentException();
        }

        targetSpeedKmh = speedKmh;
    }
}
  1. Use the type of the exception to learn more about what went wrong and communicate that in the exception message, for example:
class TransmissionParser {
    static Transmission parse(String rawMessage) {
        if (rawMessage != null
                && rawMessage.length() != Transmission.MESSAGE_LENGTH) {
            throw new IllegalArgumentException("Bad message received!");
        }

        String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
        String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
        try {
            int id = Integer.parseInt(rawId);
            String content = rawContent.trim();
            return new Transmission(id, content);
        } catch (Exception e) {
            throw new IllegalArgumentException("Bad message received!");
        }
    }
}

The exception type in catch is too general, we can first make it more specific:

class TransmissionParser {
    static Transmission parse(String rawMessage) {
        if (rawMessage != null &&
                rawMessage.length() != Transmission.MESSAGE_LENGTH) {
            throw new IllegalArgumentException("Bad message received!");
        }

        String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
        String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
        try {
            int id = Integer.parseInt(rawId);
            String content = rawContent.trim();
            return new Transmission(id, content);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Bad message received!");
        }
    }
}

We can communicate this information further using the message in the exception like so:

class TransmissionParser {
    static Transmission parse(String rawMessage) {
        if (rawMessage != null
                && rawMessage.length() != Transmission.MESSAGE_LENGTH) {
            throw new IllegalArgumentException(
                String.format("Expected %d, but got %d characters in '%s'",
                    Transmission.MESSAGE_LENGTH, rawMessage.length(),
                    rawMessage));
        }

        String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
        String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
        try {
            int id = Integer.parseInt(rawId);
            String content = rawContent.trim();
            return new Transmission(id, content);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(
                String.format("Expected number, but got '%s' in '%s'",
                        rawId, rawMessage));
        }
    }
}

You may also pass the cause chain to the exception like so:

class TransmissionParser {
    static Transmission parse(String rawMessage) {
        if (rawMessage != null
                && rawMessage.length() != Transmission.MESSAGE_LENGTH) {
            throw new IllegalArgumentException(
                String.format("Expected %d, but got %d characters in '%s'",
                        Transmission.MESSAGE_LENGTH, rawMessage.length(),
                        rawMessage));
        }

        String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
        String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
        try {
            int id = Integer.parseInt(rawId);
            String content = rawContent.trim();
            return new Transmission(id, content);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(
                String.format("Expected number, but got '%s' in '%s'",
                    rawId, rawMessage), e);
        }
    }
}

[Worksheet]