Skip to content

Example Project

Ben Nordick edited this page May 14, 2020 · 8 revisions

Hi! I'm Ben Nordick, the creator of eMPire. I think the best way to understand eMPire is to exercise it yourself and see what it can do for you. Let's build an example Machine Project together!

Project creation

Update Android Studio to version 3.6.3. Create a new project with an Empty Activity. Let's name the project "Example MP" and put it in the com.example.mp package. eMPire is designed for Java projects, so make sure Language is set to Java. The default SDK version should work, but CS 125 uses API level 24 as a minimum, so that's what I'll use here. Press Finish and Android Studio will create a new project for you with a nearly empty MainActivity. After sync completes, switch the view of the left file browser pane to Project so you can see all the files.

Machine Projects are intended to be quickly auto-gradable, so we don't really have a use for "Android test" sources, which run tests on an Android device or emulator. Once we get to writing test suites that need an Android environment, we'll use Robolectric with JUnit 4. For this chapter, though, we'll stick to plain Java tests. Clean the project up a bit by deleting the androidTest folder under src.

Gradle setup

Each Android project has two Gradle buildscripts: a build.gradle in the root of the project and another build.gradle inside the app module. The root buildscript usually only needs attention if you have other subprojects in addition to the Android app itself or if you're customizing the overall build process. We're not going to work with it here except to make sure that it depends on the version of the Android Gradle plugin that eMPire targets:

classpath 'com.android.tools.build:gradle:3.6.3'

eMPire should work fine with newer versions of Android Studio as long as the Android Gradle plugin is still version 3.6.3. If your Android Studio version came with a newer Android Gradle plugin, it's also worth checking gradle/wrapper/gradle-wrapper.properties file to make sure the project is using eMPire's preferred Gradle version, 5.6.4:

distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

Before we get too deep into things, some terminology clarification: Gradle is the build system itself. It can be used for all sorts of projects and is not specific to Android. Android Gradle is a plugin written by Google for the Gradle build system to automate the process of compiling Android apps, which is quite complex. There's nothing magical about it, though—you could in theory write a bunch of Gradle tasks yourself right in a standalone build.gradle file to wire all the build tools together and compile an app. eMPire is another Gradle plugin that interacts (interferes?) with Gradle and Android Gradle tasks to provide checkpointing functionality.

Now let's turn our attention to the app buildscript, which is where we'll configure eMPire. Android Studio generates buildscripts written in Groovy. eMPire can work with that, but if you're doing complicated things with Gradle, Kotlin buildscripts are a much better choice. I'm going to rename the app buildscript file to build.gradle.kts (note the KTS indicating "Kotlin script") and perform a mostly mechanical translation of Groovy to Kotlin, resulting in this intermediate. After changing a Gradle-related file you will be prompted to sync the project and should do so. From here on out you will occasionally get warnings about "Gradle KTS Build Files" not being fully supported, but this is harmless. To help Gradle find the eMPire plugin when we apply it in a few moments, we will need to adjust the settings script. Rename settings.gradle in the project root to settings.gradle.kts and replace its contents with:

include(":app")

pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
    resolutionStrategy {
        eachPlugin {
            if (requested.id.namespace == "edu.illinois.cs.cs125" && requested.version != null) {
                useModule("com.github.cs125-illinois:${requested.id.name}:${requested.version}")
            }
        }
    }
}

Create a file called grade.yaml in the root of the project. Students will change this to indicate what checkpoint they're working on and whether they'd like to use provided components. For now, all it needs to contain is:

useProvided: false

This tells eMPire not to swap out anything for provided components. We will work more with this file in later chapters.

Back in the app's build.gradle.kts, we're going to make several changes, ending up with:

buildscript {
    repositories {
        mavenCentral()
        google()
    }
}

plugins {
    id("com.android.application")
    id("edu.illinois.cs.cs125.empire") version "2020.1.5"
}

android {
    compileSdkVersion(29)
    buildToolsVersion("29.0.3")

    defaultConfig {
        applicationId = "com.example.mp"
        minSdkVersion(24)
        targetSdkVersion(29)
        versionCode = 1
        versionName = "1.0"
    }

    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }

    testOptions {
        unitTests.isIncludeAndroidResources = true
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

}

dependencies {
    implementation("androidx.appcompat:appcompat:1.1.0")
    implementation("androidx.constraintlayout:constraintlayout:1.1.3")
    testImplementation("junit:junit:4.13")
    testImplementation("org.robolectric:robolectric:4.3.1")
    testImplementation("androidx.test:core:1.2.0")
}

eMPire {
    studentConfig = rootProject.file("grade.yaml")
}

My changes from top to bottom:

  • The buildscriptrepositories block tells Gradle which online repositories to download plugins from. eMPire is on Maven Central.
  • The new id directive inside plugins adds the eMPire plugin to the project.
  • We don't need a testInstrumentationRunner since we're not running true Android tests.
  • Setting the testOptions to make Android resources available is necessary for Robolectric tests to simulate the app's Android environment correctly.
  • The compileOptions block allows targeting Java 8, which added lambdas.
  • All app dependencies should be fetched from an artifact repository by Gradle. We therefore don't want the fileTree dependency that looks for JARs stored locally in that libs folder.
  • We want the latest JUnit 4 revision, version 4.13.
  • The two new testImplementation dependencies are needed for writing Robolectric tests.
  • Since we're not running true Android tests, we don't need any androidTestImplementation dependencies.
  • The eMPire block configures eMPire. Specifically, it manipulates the EmpireExtension. For the project to be able to sync we just need to tell eMPire where to find the student configuration file we created. We'll add a lot more to this block as we go.

Logic classes

It is tradition to test eMPire with boring math apps. Before we dive into applying it to Android activities, we're going to make some logic classes that stick to the Java standard library. It can be nice for students to have the plain-Java classes separated from the complicated Android-related ones, so we will organize the logic classes into a subpackage here, though you are not required to do so. Create a logic subpackage under com.example.mp in the main sources. Inside it, create a new Java class called ComplexNumber:

package com.example.mp.logic;

public class ComplexNumber {
    private int real;
    private int imaginary;
    public ComplexNumber(int setReal, int setImaginary) {
        real = setReal;
        imaginary = setImaginary;
    }
    public int getReal() {
        return real;
    }
    public int getImaginary() {
        return imaginary;
    }
    public ComplexNumber plus(ComplexNumber other) {
        return new ComplexNumber(real + other.real, imaginary + other.imaginary);
    }
    public ComplexNumber times(ComplexNumber other) {
        // Wrong!
        return new ComplexNumber(real * other.real, imaginary * other.imaginary);
    }
}

The multiplication function times is, as the comment says, wrong. (You don't have to know how complex arithmetic works to follow along, but here's some notes for the curious.) Imagine this is a student submission we're grading. Let's write a test suite to check the class's correctness. Android Studio created an example JUnit test for us in the test sources, so let's take advantage of that. Refactor | Rename ExampleUnitTest to Checkpoint0Test and write some tests:

package com.example.mp;

import com.example.mp.logic.ComplexNumber;

import org.junit.Test;

import static org.junit.Assert.*;

public class Checkpoint0Test {
    @Test
    public void constructor_remembersParameters() {
        ComplexNumber n = new ComplexNumber(3, 5);
        assertEquals(3, n.getReal());
        assertEquals(5, n.getImaginary());
    }
    @Test
    public void plus_isCorrect() {
        ComplexNumber a = new ComplexNumber(2, 0);
        ComplexNumber b = new ComplexNumber(4, -1);
        ComplexNumber sum = a.plus(b);
        assertEquals(6, sum.getReal());
        assertEquals(-1, sum.getImaginary());
    }
    @Test
    public void times_isCorrect() {
        ComplexNumber a = new ComplexNumber(2, 3);
        ComplexNumber b = new ComplexNumber(-1, 2);
        ComplexNumber product = a.times(b);
        assertEquals(-8, product.getReal());
        assertEquals(1, product.getImaginary());
    }
}

Run it with the green arrow in the margin next to the class name and, sure enough, all tests except times_isCorrect pass. Suppose we've released a new checkpoint and the student is supposed to build on their ComplexNumber work. Create a ComplexPolynomial class in the subpackage in the main source set:

package com.example.mp.logic;

public class ComplexPolynomial {
    private ComplexNumber[] coefficients; // f(x) = c[0] + x*c[1] + x*x*c[2] + ...
    public ComplexPolynomial(ComplexNumber... setCoefficients) {
        coefficients = setCoefficients;
    }
    public ComplexNumber evaluate(ComplexNumber x) {
        ComplexNumber result = new ComplexNumber(0, 0);
        for (int power = 0; power < coefficients.length; power++) {
            ComplexNumber term = coefficients[power];
            for (int multiplications = 0; multiplications < power; multiplications++) {
                term = term.times(x);
            }
            result = result.plus(term);
        }
        return result;
    }
    public ComplexPolynomial algebraicPlus(ComplexPolynomial other) {
        // Wrong! Can throw ArrayIndexOutOfBoundsException
        ComplexNumber[] newCoefficients = new ComplexNumber[coefficients.length];
        for (int c = 0; c < newCoefficients.length; c++) {
            newCoefficients[c] = coefficients[c].plus(other.coefficients[c]);
        }
        return new ComplexPolynomial(newCoefficients);
    }
}

This polynomial addition implementation is wrong because it will run off the end of an array if the other polynomial has fewer terms than this one. Sadly for this student, evaluate is correct but will not produce the right results because of the dependency on ComplexNumber#times, which is incorrect. We will address this in a later chapter. For now, let's write some more tests. Create a Checkpoint1Test class in the test sources:

package com.example.mp;

import com.example.mp.logic.ComplexNumber;
import com.example.mp.logic.ComplexPolynomial;

import org.junit.Test;

import static org.junit.Assert.*;

public class Checkpoint1Test {
    @Test
    public void evaluate_isCorrect() {
        ComplexNumber constant = new ComplexNumber(5, 1);
        ComplexNumber linear = new ComplexNumber(-1, 2);
        ComplexPolynomial polynomial = new ComplexPolynomial(constant, linear);
        ComplexNumber result = polynomial.evaluate(new ComplexNumber(2, 3));
        assertEquals(-3, result.getReal());
        assertEquals(2, result.getImaginary());
    }
    @Test
    public void algebraicPlus_isCorrect() {
        ComplexNumber constant = new ComplexNumber(0, 1);
        ComplexNumber linear = new ComplexNumber(2, 0);
        ComplexPolynomial linearPolynomial = new ComplexPolynomial(constant, linear);
        ComplexNumber extraConstant = new ComplexNumber(1, 0);
        ComplexPolynomial constantPolynomial = new ComplexPolynomial(extraConstant);
        ComplexPolynomial addedPolynomial = linearPolynomial.algebraicPlus(constantPolynomial);
        ComplexNumber result = addedPolynomial.evaluate(new ComplexNumber(1, 0));
        assertEquals(3, result.getReal());
        assertEquals(1, result.getImaginary());
    }
}

Run it and see that both tests fail, evaluate_isCorrect with an assertion error and algebraicPlus_isCorrect with an out-of-bounds crash.

In the next chapter we'll use eMPire to compile these test classes separately.

Clone this wiki locally