Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable pre-dexing and animations on your build server #1

Open
ardock opened this issue Nov 22, 2015 · 11 comments
Open

Disable pre-dexing and animations on your build server #1

ardock opened this issue Nov 22, 2015 · 11 comments

Comments

@ardock
Copy link
Owner

ardock commented Nov 22, 2015

In general build system will always perform clean builds and this pre-dexing becomes a penalty.

Tests now fail on devices with animations enabled since AnimationAwareTestRule was added.

I'll explain here my changes to disable pre-dexing and animations on your build server.

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

Disable pre-dexing only on your build server

First, in your root build.gradle add the following:

// Improve Build Server performance.
// See Tip 3: http://tools.android.com/tech-docs/new-build-system/tips
project.ext.preDexLibs = !project.hasProperty('disablePreDex')

subprojects {
    // Disable pre-dexing only on your build server.
    project.plugins.whenPluginAdded { plugin ->
        if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) {
            project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
        } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) {
            project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
        }
    }
}

Then configure your build server to call Gradle tasks with -PdisablePreDex like this:

./gradlew clean cAT -PdisablePreDex

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

Disable pre-dexing on Travis-ci, compare results using a Build Matrix, use a bash one-liner to selectively export the environment variable, and avoid build timeouts using travis_wait:

matrix:
  include:
    - env: AVD_TARGET=android-17
    - env: AVD_TARGET=android-17 ARGS='-PdisablePreDex'
before_install:
  - if [[ "${ARGS:-}" == *-PdisablePreDex* ]]; then export PREDEX_OPT="-PdisablePreDex"; fi;
script:
  - travis_wait ./gradlew cAT  ${PREDEX_OPT:-} # Install and runs android tests for all flavors

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

You can print test results directly from the after_script section but I'm moving the code to scripts.

after_script:
 - ./execute show-travis-test-results # Print unit and intrumentation test results on Travis-ci web

I created the execute and acib scripts for fun to be used without warranties or conditions of any kind.

Currently only available on my temporal travis tests like the animation aware awesome branches ttt-aaa*.

# Show unit and instrumentation test results
function show-travis-test-results {
    print-format cyan "Unit Tests:"
    echo
    cat ${TRAVIS_BUILD_DIR}/*/build/test-results/debug/*
    cat ${TRAVIS_BUILD_DIR}/*/build/test-results/release/*
    print-format cyan "Android Tests:"
    echo
    cat ${TRAVIS_BUILD_DIR}/*/build/outputs/androidTest-results/connected/*
    print-format reset
    echo
}

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

The Espresso Team explained how to disable animations reflectively two years ago:

System animations can introduce timing errors that make your tests flaky. Google's test runner infrastructure uses an instrumentation to turn off animations before running tests. A similar approach can be adapted to other CI builds. Here's an example instrumentation that can turn off animations.

public final class Primer extends Instrumentation {
  private static final String TAG = "Primer";
  private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE";

  @Override
  public void onCreate(Bundle args) {
    // as time goes on we may actually need to process our arguments.
    disableAnimation();
  }

  private void disableAnimation() {
    int permStatus = getContext().checkCallingOrSelfPermission(ANIMATION_PERMISSION);
    if (permStatus == PackageManager.PERMISSION_GRANTED) {
      if (reflectivelyDisableAnimation()) {
        Log.i(TAG, "All animations disabled.");
      } else {
        Log.i(TAG, "Could not disable animations.");
      }
    } else {
      Log.i(TAG, "Cannot disable animations due to lack of permission.");
    }
  }

  private boolean reflectivelyDisableAnimation() {
    try {
      Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
      Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
      Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
      Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
      Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
      Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales",
          float[].class);
      Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");

      IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
      Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
      float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
      for (int i = 0; i < currentScales.length; i++) {
        currentScales[i] = 0.0f;
      }
      setAnimationScales.invoke(windowManagerObj, currentScales);
      return true;
    } catch (ClassNotFoundException cnfe) {
      Log.w(TAG, "Cannot disable animations reflectively.", cnfe);
    } catch (NoSuchMethodException mnfe) {
      Log.w(TAG, "Cannot disable animations reflectively.", mnfe);
    } catch (SecurityException se) {
      Log.w(TAG, "Cannot disable animations reflectively.", se);
    } catch (InvocationTargetException ite) {
      Log.w(TAG, "Cannot disable animations reflectively.", ite);
    } catch (IllegalAccessException iae) {
      Log.w(TAG, "Cannot disable animations reflectively.", iae);
    } catch (RuntimeException re) {
      Log.w(TAG, "Cannot disable animations reflectively.", re);
    }
    return false;
  }
}

You can find different approaches on comments via gist files like this, see the forks, or this blog.

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

Recently Ben Weiss, the owner of android-topeka project, added this nice AnimationAwareTestRule:

/**
 * A test rule that will fail a test if animations or transitions are enabled on a device.
 */
public class AnimationAwareTestRule implements TestRule {

    private static final String TAG = "AnimationAware";

    public AnimationAwareTestRule() { }

    @Override
    public Statement apply(Statement base, Description description) {
        return new AnimationAwareStatement(base);
    }

    private class AnimationAwareStatement extends Statement {

        private Statement mBase;

        public AnimationAwareStatement(Statement base) {
            mBase = base;
        }

        @Override
        public void evaluate() throws Throwable {
            checkForDisabledAnimationsAndTransitions(InstrumentationRegistry.getTargetContext());
            mBase.evaluate();
        }
    }
...

Tests now fail on devices with animations enabled since AnimationAwareTestRule was added.

But this difficult to us to use CI build servers because we need to disable animations on emulators.

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

I added three new test rules to the new rule folder, see ttt-aaa branches, that extend an abstract class:

/**
 * A base class for triple-A Rules that control animation scales on devices to avoid timing errors.
 */
public abstract class AnimationAwareAwesomeTestRule implements TestRule {

    @Override
    public Statement apply(Statement base, Description description) {
        return new AnimationAwareAwesomeStatement(base);
    }

    private class AnimationAwareAwesomeStatement extends Statement {

        private Statement mBase;

        public AnimationAwareAwesomeStatement(Statement base) {
            mBase = base;
        }

        @Override
        public void evaluate() throws Throwable {
            before();
            try {
                mBase.evaluate();
            } finally {
                after();
            }
        }
    }

    /**
     * Override to set up your specific animation aware rule. Disable animations for example.
     *
     * @throws Throwable if setup fails (which will disable {@code after()} method).
     */
    protected void before() throws Throwable {
        // do nothing
    }

    /**
     * Override to tear down your specific animation aware rule. Re-enable animations for example.
     *
     * @throws Throwable if tear down fails (which would affect future animation related tests).
     */
    protected void after() throws Throwable {
        // do nothing
    }
}

AnimationAwareReaderTestRule and AnimationAwareReader helper class.
AnimationAwareWriterTestRule and AnimationAwareWriter helper class.
AnimationAwareWonderTestRule and AnimationAwareWonder helper class.

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

I splitted the original test rule into AnimationAwareReaderTestRule and AnimationAwareReader helper.

/**
 * A test rule that will fail a test if animations or transitions are enabled on a device.
 */
public class AnimationAwareReaderTestRule extends AnimationAwareAwesomeTestRule {

    @Override
    protected void before() throws Throwable {
        AnimationAwareReader.checkForDisabledAnimationsAndTransitions();
    }
}

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

I extended the previous helper combined with reflection to disable and enable animations and transitions.

/**
 * A test rule that will fail a test if animations are enabled and cannot be temporarily disabled.
 */
public class AnimationAwareWriterTestRule extends AnimationAwareAwesomeTestRule {

    private boolean mDisabled;

    @Override
    protected void before() throws Throwable {
        if (AnimationAwareWriter.isWritePermissionDenied()) {
            AnimationAwareReader.checkForDisabledAnimationsAndTransitions();
        } else if (AnimationAwareReader.isAnyAnimationEnabled()) {
            mDisabled = AnimationAwareWriter.tryToDisableAnimationsAndTransitions();
        }
    }

    @Override
    protected void after() throws Throwable {
        if (mDisabled) {
            AnimationAwareWriter.tryToEnableAnimationsAndTransitions();
        }
    }
}

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

I combined both solutions to save, disable and restore the exact animation scales.

/**
 * A test rule that will fail a test if animations are enabled and cannot be disabled and restored.
 */
public class AnimationAwareWonderTestRule extends AnimationAwareAwesomeTestRule {

    private float[] mAnimationScales;

    @Override
    protected void before() throws Throwable {
        mAnimationScales = AnimationAwareWonder.tryToRetrieveAndDisableAnimationsAndTransitions();
    }

    @Override
    protected void after() throws Throwable {
        AnimationAwareWonder.tryToRestoreAndEnableAnimationsAndTransitions(mAnimationScales);
    }
}

You need to find the original test rule usages (Alt+F7) and replace it by the new rule you want to use.

You also need to grant the SET_ANIMATION_SCALE permission for testing.

@ardock
Copy link
Owner Author

ardock commented Nov 22, 2015

In this case, following the commented gist files, create a new build.gradle file in the app module:

def adb = android.getAdbExe().toString()

task grantSetAnimationScalePermission(type: Exec, dependsOn: 'installDebug') {
    group = 'verification'
    description = 'Grant SET_ANIMATION_SCALE permission for testing.'
    def saspermission = 'android.permission.SET_ANIMATION_SCALE'
    commandLine "$adb shell pm grant $android.defaultConfig.applicationId $saspermission".split(' ')
}

tasks.whenTaskAdded { task ->
    if (task.name.startsWith('connectedDebugAndroidTest')) {
        task.dependsOn grantSetAnimationScalePermission
    }
}

I named it grant-set-animation-scale-permission.gradle, perhaps too long.

Add the next line at the bottom of your module build.gradle file:

apply from: "grant-set-animation-scale-permission.gradle" 

And finally, in this case, add the next line to the already created debug manifest:

    <!-- required to temporarily disable animation scales on devices to avoid timing errors -->
    <uses-permission android:name="android.permission.SET_ANIMATION_SCALE" />

Repository owner locked and limited conversation to collaborators Nov 23, 2015
@ardock
Copy link
Owner Author

ardock commented Aug 17, 2016

Test rules updated here. Two nice alternatives to disable animations and more:

For Circle-CI users, this great post Setup Circle CI: Running Android Tests & Code Coverage Reports.

Trying to get help to solve API 22 & 23 issues here and here: Cappuccino plugin also disables animations.

API 22 issue is solved but API 23 emulators are bugged and need this fix.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

1 participant