FlutterCodeX: A Code Coverage Solution for Flutter

By Junai at Xianyu Technology

Background

Manual inspection largely relies on the business familiarity of developers and can go wrong due to negligence. Therefore, we need accurate statistics on code coverage information to enable code downsizing for businesses.

This article describes a code coverage solution for Flutter, that is, FlutterCodeX. This solution obtains code coverage by collecting and uploading statistics, according to the vcode instrumentation during class compilation and runtime data aggregation. FlutterCodeX promotes the deprecation of abandoned services to reduce the package size. At the same time, it monitors and improves engineering health.

Exploration of Code Instrumentation

iOS

static BOOL MOCClassIsInitilatized(Class cls) {
void *metaClass = (__bridge void *)object_getClass(cls);
class_rw_t *rw = *(class_rw_t **)((uintptr_t)metaClass + 4 * sizeof(uintptr_t));
if(((class_rw_t *)((uintptr_t)rw & FAST_DATA_MASK))->flags & RW_INITIALIZED) {
return YES;
}
return NO;
}

Android

public class A {
static {
// todo report class A initialize
}
}

Flutter

Theoretically, the initialization sequence of Dart Class is as follows:

  1. Class variables initialize on declaration (no static)
  2. Initializer list
  3. Superclass’s constructor
  4. MainClass’s constructor

By overriding the constructors, we can directly intrude into the original code. The diversity of Dart constructors also increases the difficulty of instrumentation automation. Therefore, overriding the constructors is not the best choice. Then, we are wondering, based on the initialization sequence, can we add new class members so that the instrumentation code can be called during initialization? For example,

class A {
bool isCodeX = ReportUtil.addCallTime('A');
// ...biz
}

However, in Dart, for a class with constant constructors, all its members are required to be final, and the members must be initialized in phases 1 and 2 or in the input parameters of the constructors. Even if a class is defined with the Extends clause or the With clause, the class also requires that all its subclasses and variables of its Mixins are final. Common components in Flutter, such as widgets, all use constant constructors. Therefore, instrumentation code cannot be inserted as class members.

class A {
final num x, y;
const A(this.x, this.y);
}

Code injection fails.

Is there any other way to do this? Can we hook up all the constructors through Aspect Oriented Programming (AOP)? Yes. AspectD, which has just been opened source by the Xianyu technical team, is ideal to this problem.

AspectD is an AOP programming framework for Dart, which implements dill transformation through Transform. AspectD can conveniently inject code in a non-invasive way.

In Flutter v1.12.13, the code injection test passes for these types of constructors, that is, constant constructors, no constructors, and constructors named as ClassName.identifier. The AspectD code is as follows:

@Aspect()
@pragma("vm:entry-point")
class CodeXExecute {
@pragma("vm:entry-point")
CodeXExecute();
@Call("package:flutter_codex_demo/test.dart", "A", "+A")
@pragma("vm:entry-point")
void _incrementA(PointCut pointcut) {
pointcut.proceed();
// todo report class A initialize
}
}

The working principle of AspectD will not be described in detail here. For more information, see https://github.com/alibaba-flutter/aspectd

Overall Design

The Code Instrumentation Plug-in

CodeAstVisitor:// visit all class
void visitClassDeclaration(ClassDeclaration node) {
SourceNode sourceNode = SourceNode(source_path, node.name?.name);
node.members.forEach((ClassMember member) {
// find all constructor
if (member is ConstructorDeclaration) {
String constructorName = member.name?.name;
if (constructorName == null || constructorName.isEmpty) {
// ClassName Constructor
constructorName = sourceNode.name;
} else {
// ClassName.identifier Constructor
constructorName = (sourceNode.name ?? '') + "\\." + constructorName;
}
sourceNode.constructor.add(constructorName);
return;
}});
CodeXGenerator.collector.codeList[sourceNode.key()] = sourceNode;
}

The code generated by AspectD Execute is shown in the following figure. Class A has two constructors, and therefore two AspectD AOP functions are generated.

Runtime Data Collection Module

Data Aggregation and Output

Take a Demo project as an example:

Summary

Original Source:

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.