Skip to content

Commit 48e3fc2

Browse files
jonpryordellis1972
authored andcommitted
[tests] Run .apk unit tests and collect TestResults.xml (#305)
What do we want? Execution of unit tests! When do we want it? Uh...4 months ago? The `Xamarin.Android.NUnitLite` assembly allows writing NUnit-based unit tests that execute on-device, and the `Mono.Android-Tests.csproj` project contains a number of such unit tests. The problem is that these unit tests aren't executed as part of the Jenkins build process, so there's no way to know if a given commit actually breaks anything. In short, the existence of those unit tests is meaningless. The task, then, is to fix the `make run-all-tests` target so that it runs on-device unit tests...on an Android device. Which raises all manner of problems. :-) (Hence 4+ months!) For starters, our internal tooling is a horrible mish-mash of make(1), ruby(1), bash(1), which creates an emulator, launches it, installs the test .apk onto the emulator, and runs the tests. I don't want all of that "cruft" in this repo, which means it needs to be rewritten in a form more amenable to this repo: MSBuild tasks....and some make(1). :-) Add a new `build-tools/scripts/UnitTestApks.targets` file, which will process an `@(UnitTestApk)` Item Group to permit deploying, running, and undeploying test .apks from an attached Android device. Add a slew of MSBuild tasks to support `@(UnitTestApk)`. Update the default `$(AndroidSupportedTargetJitAbis)` value to be `armeabi-v7a:x86`. The created Android emulator is x86.
1 parent 908f56d commit 48e3fc2

File tree

26 files changed

+956
-114
lines changed

26 files changed

+956
-114
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ packages
88
.DS_Store
99
.nuget
1010
TestResult.xml
11+
TestResult-*.xml

Configuration.props

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<AndroidSdkDirectory>$(AndroidToolchainDirectory)\sdk</AndroidSdkDirectory>
3939
<AndroidNdkDirectory>$(AndroidToolchainDirectory)\ndk</AndroidNdkDirectory>
4040
<AndroidSupportedHostJitAbis Condition=" '$(AndroidSupportedHostJitAbis)' == '' ">$(HostOS)</AndroidSupportedHostJitAbis>
41-
<AndroidSupportedTargetJitAbis Condition=" '$(AndroidSupportedTargetJitAbis)' == '' ">armeabi-v7a</AndroidSupportedTargetJitAbis>
41+
<AndroidSupportedTargetJitAbis Condition=" '$(AndroidSupportedTargetJitAbis)' == '' ">armeabi-v7a:x86</AndroidSupportedTargetJitAbis>
4242
<JavaInteropSourceDirectory Condition=" '$(JavaInteropSourceDirectory)' == '' ">$(MSBuildThisFileDirectory)external\Java.Interop</JavaInteropSourceDirectory>
4343
<LlvmSourceDirectory Condition=" '$(LlvmSourceDirectory)' == '' ">$(MSBuildThisFileDirectory)external\llvm</LlvmSourceDirectory>
4444
<MonoSourceDirectory>$(MSBuildThisFileDirectory)external\mono</MonoSourceDirectory>
@@ -70,6 +70,14 @@
7070
<LibZipSourceFullPath>$([System.IO.Path]::GetFullPath ('$(LibZipSourceDirectory)'))</LibZipSourceFullPath>
7171
<LibZipSharpSourceFullPath>$([System.IO.Path]::GetFullPath ('$(LibZipSharpSourceDirectory)'))</LibZipSharpSourceFullPath>
7272
</PropertyGroup>
73+
<PropertyGroup>
74+
<AdbToolPath Condition=" '$(AdbToolPath)' == '' ">$(AndroidSdkFullPath)\platform-tools</AdbToolPath>
75+
<AdbToolExe Condition=" '$(AdbToolExe)' == '' ">adb</AdbToolExe>
76+
<AndroidToolPath Condition=" '$(AndroidToolPath)' == '' ">$(AndroidSdkFullPath)\tools</AndroidToolPath>
77+
<AndroidToolExe Condition=" '$(AndroidToolExe)' == '' ">android</AndroidToolExe>
78+
<EmulatorToolPath Condition=" '$(EmulatorToolPath)' == '' ">$(AndroidSdkFullPath)\tools</EmulatorToolPath>
79+
<EmulatorToolExe Condition=" '$(EmulatorToolExe)' == '' ">emulator</EmulatorToolExe>
80+
</PropertyGroup>
7381
<!--
7482
"Fixup" $(AndroidSupportedHostJitAbis) so that Condition attributes elsewhere
7583
can use `:ABI-NAME:`, to avoid substring mismatches.

Documentation/DevelopmentTips.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Development Tips
2+
3+
Tips and tricks while developing Xamarin.Android.
4+
5+
# How do I rebuild the Mono Runtime and Native Binaries?
6+
7+
The various Mono runtimes -- over *20* of them (!) -- all store object code
8+
within `build-tools/mono-runtimes/obj/$(Configuration)/TARGET`.
9+
10+
If you change sources within `external/mono`, a top-level `make`/`xbuild`
11+
invocation may not rebuild those mono native binaries. To explicitly rebuild
12+
*all* Mono runtimes, use the `ForceBuild` target:
13+
14+
# Build and install all runtimes
15+
$ xbuild /t:ForceBuild build-tools/mono-runtimes/mono-runtimes.mdproj
16+
17+
To build Mono for a specific target, run `make` from the relevant directory
18+
and invoke the `_InstallRuntimes` target. For example, to rebuild
19+
Mono for armeabi-v7a:
20+
21+
$ cd build-tools/mono-runtimes
22+
$ make -C obj/Debug/armeabi-v7a
23+
24+
# This updates bin/$(Configuration)/lib/xbuild/Xamarin/Android/lib/armeabi-v7a/libmonosgen-2.0.so
25+
$ xbuild /t:_InstallRuntimes
26+
27+
# How do I rebuild BCL assemblies?
28+
29+
The Xamarin.Android Base Class Library assemblies, such as `mscorlib.dll`,
30+
are built within `external/mono`, using Mono's normal build system:
31+
32+
# This updates external/mono/mcs/class/lib/monodroid/ASSEMBLY.dll
33+
$ make -C external/mono/mcs/class/ASSEMBLY PROFILE=monodroid
34+
35+
Alternatively, if you want to rebuild *all* the assemblies, the "host"
36+
Mono needs to be rebuilt. Note that the name of the "host" directory
37+
varies based on the operating system you're building from:
38+
39+
$ make -C build-tools/mono-runtimes/obj/Debug/host-Darwin
40+
41+
Once the assemblies have been rebuilt, they can be copied into the appropriate
42+
Xamarin.Android SDK directory by using the `_InstallBcl` target:
43+
44+
# This updates bin/$(Configuration)/lib/xbuild-frameworks/MonoAndroid/v1.0/ASSEMBLY.dll
45+
$ xbuild build-tools/mono-runtimes/mono-runtimes.mdproj /t:_InstallBcl
46+
47+
# Testing Updated Assemblies
48+
49+
The `xamarin-android` repo does not support [fast deployment][fastdep],
50+
which means that, *normally*, if you wanted to e.g. test a fix within
51+
`Mono.Android.dll` you would need to:
52+
53+
[fastdev]: https://developer.xamarin.com/guides/android/under_the_hood/build_process/#Fast_Deployment
54+
55+
1. Build `src/Mono.Android/Mono.Android.csproj`
56+
2. Rebuild your test project, e.g.
57+
`src/Mono.Android/Test/Mono.Android-Tests.csproj`
58+
3. Reinstall the test project
59+
4. Re-run the test project.
60+
61+
The resulting `.apk`s can be quite big, e.g.
62+
`bin/TestDebug/Mono.Android_Tests-Signed.apk` is 59MB, so steps
63+
(2) through (4) can be annoyingly time consuming.
64+
65+
Fortunately, a key part of fast deployment *is* part of the `xamarin-android`:
66+
an "update directory" is created by `libmono-android*.so` during process
67+
startup, in *non*-`RELEASE` builds. This directory is printed to `adb logcat`:
68+
69+
W/monodroid( 2796): Creating public update directory: `/data/data/Mono.Android_Tests/files/.__override__`
70+
71+
Assemblies located within the "update directory" are used *in preference to*
72+
assemblies located within the executing `.apk`. Assemblies can be `adb push`ed
73+
into the update directory:
74+
75+
adb push bin/Debug/lib/xbuild-frameworks/MonoAndroid/v7.1/Mono.Android.dll /data/data/Mono.Android_Tests/files/.__override__
76+
77+
When the process restarts the new assembly will be used.

Makefile

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ all::
2525
$(MSBUILD) $(MSBUILD_FLAGS) $(SOLUTION)
2626

2727
all-tests::
28-
tools/scripts/xabuild $(MSBUILD_FLAGS) Xamarin.Android-Tests.sln
28+
MSBUILD="$(MSBUILD)" tools/scripts/xabuild $(MSBUILD_FLAGS) Xamarin.Android-Tests.sln
2929

3030
prepare:: prepare-external prepare-props
3131

@@ -142,27 +142,25 @@ define RUN_NUNIT_TEST
142142
$(RUNTIME) --runtime=v4.0.0 \
143143
$(NUNIT_CONSOLE) $(NUNIT_EXTRA) $(1) \
144144
$(if $(RUN),-run:$(RUN)) \
145+
--result=TestResult-$(basename $(notdir $(1))).xml \
145146
-output=bin/Test$(CONFIGURATION)/TestOutput-$(basename $(notdir $(1))).txt ;
146147
endef
147148

148149
run-nunit-tests: $(NUNIT_TESTS)
149150
$(foreach t,$(NUNIT_TESTS), $(call RUN_NUNIT_TEST,$(t),1))
150151

151-
# Test .apk projects must satisfy the following requirements:
152-
# 1. They must have a UnDeploy target
153-
# 2. They must have a Deploy target
154-
# 3. They must have a RunTests target
152+
# .apk files to test on-device need to:
153+
# (1) Have their .csproj files listed here
154+
# (2) Add a `@(UnitTestApk)` entry to `tests/RunApkTests.targets`
155155
TEST_APK_PROJECTS = \
156156
src/Mono.Android/Test/Mono.Android-Tests.csproj
157157

158-
# Syntax: $(call RUN_TEST_APK,path/to/project.csproj)
159-
define RUN_TEST_APK
158+
# Syntax: $(call BUILD_TEST_APK,path/to/project.csproj)
159+
define BUILD_TEST_APK
160160
# Must use xabuild to ensure correct assemblies are resolved
161-
tools/scripts/xabuild /t:SignAndroidPackage $(1) && \
162-
$(MSBUILD) $(MSBUILD_FLAGS) /t:UnDeploy $(1) && \
163-
$(MSBUILD) $(MSBUILD_FLAGS) /t:Deploy $(1) && \
164-
$(MSBUILD) $(MSBUILD_FLAGS) /t:RunTests $(1) $(if $(ADB_TARGET),"/p:AdbTarget=$(ADB_TARGET)",)
165-
endef
161+
MSBUILD="$(MSBUILD)" tools/scripts/xabuild /t:SignAndroidPackage $(1)
162+
endef # BUILD_TEST_APK
166163

167164
run-apk-tests:
168-
$(foreach p, $(TEST_APK_PROJECTS), $(call RUN_TEST_APK, $(p)))
165+
$(foreach p, $(TEST_APK_PROJECTS), $(call BUILD_TEST_APK, $(p)))
166+
$(MSBUILD) $(MSBUILD_FLAGS) tests/RunApkTests.targets

README.md

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -290,44 +290,4 @@ We use [Bugzilla](https://bugzilla.xamarin.com/enter_bug.cgi?product=Android) to
290290

291291
# Maintainer FAQ
292292

293-
## How do I rebuild the Mono Runtime and Native Binaries?
294-
295-
The various Mono runtimes -- over *20* of them (!) -- all store object code
296-
within `build-tools/mono-runtimes/obj/$(Configuration)/TARGET`.
297-
298-
If you change sources within `external/mono`, a top-level `make`/`xbuild`
299-
invocation may not rebuild those mono native binaries. To explicitly rebuild
300-
*all* Mono runtimes, use the `ForceBuild` target:
301-
302-
# Build and install all runtimes
303-
$ xbuild /t:ForceBuild build-tools/mono-runtimes/mono-runtimes.mdproj
304-
305-
To build Mono for a specific target, run `make` from the relevant directory
306-
and invoke the `_InstallRuntimes` target. For example, to rebuild
307-
Mono for armeabi-v7a:
308-
309-
$ cd build-tools/mono-runtimes
310-
$ make -C obj/Debug/armeabi-v7a
311-
312-
# This updates bin/$(Configuration)/lib/xbuild/Xamarin/Android/lib/armeabi-v7a/libmonosgen-2.0.so
313-
$ xbuild /t:_InstallRuntimes
314-
315-
## How do I rebuild BCL assemblies?
316-
317-
The Xamarin.Android Base Class Library assemblies, such as `mscorlib.dll`,
318-
are built within `external/mono`, using Mono's normal build system:
319-
320-
# This updates external/mono/mcs/class/lib/monodroid/ASSEMBLY.dll
321-
$ make -C external/mono/mcs/class/ASSEMBLY PROFILE=monodroid
322-
323-
Alternatively, if you want to rebuild *all* the assemblies, the "host"
324-
Mono needs to be rebuilt. Note that the name of the "host" directory
325-
varies based on the operating system you're building from:
326-
327-
$ make -C build-tools/mono-runtimes/obj/Debug/host-Darwin
328-
329-
Once the assemblies have been rebuilt, they can be copied into the appropriate
330-
Xamarin.Android SDK directory by using the `_InstallBcl` target:
331-
332-
# This updates bin/$(Configuration)/lib/xbuild-frameworks/MonoAndroid/v1.0/ASSEMBLY.dll
333-
$ xbuild build-tools/mono-runtimes/mono-runtimes.mdproj /t:_InstallBcl
293+
See [DevelopmentTips.md](Documentation/DevelopmentTips.md).

build-tools/android-toolchain/android-toolchain.projitems

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@
8686
<HostOS></HostOS>
8787
<DestDir>extras\android\m2repository</DestDir>
8888
</AndroidSdkItem>
89+
<AndroidSdkItem Include="sysimg_x86-21_r03.zip">
90+
<HostOS></HostOS>
91+
<RelUrl>sys-img/android/</RelUrl>
92+
<DestDir>system-images\android-21\x86</DestDir>
93+
</AndroidSdkItem>
8994
</ItemGroup>
9095
<ItemGroup>
9196
<_NdkToolchain Include="arm-linux-androideabi-clang" Condition="$(AndroidSupportedTargetJitAbisForConditionalChecks.Contains(':armeabi:')) Or $(AndroidSupportedTargetJitAbisForConditionalChecks.Contains(':armeabi-v7a:')) Or $(AndroidSupportedTargetAotAbisForConditionalChecks.Contains (':win-armeabi:'))">

build-tools/android-toolchain/android-toolchain.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
Outputs="@(_PlatformAndroidSdkItem->'$(AndroidToolchainCacheDirectory)\%(Identity)');@(_PlatformAndroidNdkItem->'$(AndroidToolchainCacheDirectory)\%(Identity)')">
3333
<MakeDir Directories="$(AndroidToolchainCacheDirectory)" />
3434
<DownloadUri
35-
SourceUris="@(_PlatformAndroidSdkItem->'$(AndroidUri)/%(Identity)');@(_PlatformAndroidNdkItem->'$(AndroidUri)/%(Identity)')"
35+
SourceUris="@(_PlatformAndroidSdkItem->'$(AndroidUri)/%(RelUrl)%(Identity)');@(_PlatformAndroidNdkItem->'$(AndroidUri)/%(RelUrl)%(Identity)')"
3636
DestinationFiles="@(_PlatformAndroidSdkItem->'$(AndroidToolchainCacheDirectory)\%(Identity)');@(_PlatformAndroidNdkItem->'$(AndroidToolchainCacheDirectory)\%(Identity)')"
3737
/>
3838
</Target>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<UsingTask AssemblyFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\Xamarin.Android.Tools.BootstrapTasks.dll" TaskName="Xamarin.Android.Tools.BootstrapTasks.Adb" />
4+
<UsingTask AssemblyFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\Xamarin.Android.Tools.BootstrapTasks.dll" TaskName="Xamarin.Android.Tools.BootstrapTasks.CheckAdbTarget" />
5+
<UsingTask AssemblyFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\Xamarin.Android.Tools.BootstrapTasks.dll" TaskName="Xamarin.Android.Tools.BootstrapTasks.CreateAndroidEmulator" />
6+
<UsingTask AssemblyFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\Xamarin.Android.Tools.BootstrapTasks.dll" TaskName="Xamarin.Android.Tools.BootstrapTasks.RunInstrumentationTests" />
7+
<UsingTask AssemblyFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\Xamarin.Android.Tools.BootstrapTasks.dll" TaskName="Xamarin.Android.Tools.BootstrapTasks.StartAndroidEmulator" />
8+
9+
<PropertyGroup>
10+
<_TestImageName>XamarinAndroidUnitTestRunner</_TestImageName>
11+
<_AdbEmulatorPort>5600</_AdbEmulatorPort>
12+
</PropertyGroup>
13+
14+
<Target Name="AcquireAndroidTarget">
15+
<CheckAdbTarget
16+
Condition=" '$(RequireNewEmulator)' != 'True' "
17+
AdbTarget="$(AdbTarget)"
18+
ToolPath="$(AdbToolPath)">
19+
<Output TaskParameter="AdbTarget" PropertyName="_AdbTarget" />
20+
<Output TaskParameter="IsValidTarget" PropertyName="_ValidAdbTarget" />
21+
</CheckAdbTarget>
22+
<CreateAndroidEmulator
23+
Condition=" '$(_ValidAdbTarget)' != 'True' "
24+
AndroidAbi="x86"
25+
AndroidSdkHome="$(AndroidSdkDirectory)"
26+
SdkVersion="21"
27+
ImageName="$(_TestImageName)"
28+
ToolExe="$(AndroidToolExe)"
29+
ToolPath="$(AndroidToolPath)"
30+
/>
31+
<StartAndroidEmulator
32+
Condition=" '$(_ValidAdbTarget)' != 'True' "
33+
AndroidSdkHome="$(AndroidSdkDirectory)"
34+
ImageName="$(_TestImageName)"
35+
Port="$(_AdbEmulatorPort)"
36+
ToolExe="$(EmulatorToolExe)"
37+
ToolPath="$(EmulatorToolPath)">
38+
<Output TaskParameter="AdbTarget" PropertyName="_AdbTarget" />
39+
<Output TaskParameter="AdbTarget" PropertyName="_EmuTarget" />
40+
<Output TaskParameter="AdbProcess" PropertyName="_EmuProcess" />
41+
</StartAndroidEmulator>
42+
<Adb
43+
Condition=" '$(_ValidAdbTarget)' != 'True' "
44+
Arguments="$(_AdbTarget) wait-for-device"
45+
/>
46+
<Message
47+
Condition=" '$(_EmuTarget)' != '' "
48+
Text="Launched Android emulator; `adb` target: '$(_AdbTarget)'"
49+
/>
50+
</Target>
51+
52+
<Target Name="ReleaseAndroidTarget">
53+
<Adb
54+
Condition=" '$(_EmuTarget)' != '' "
55+
Arguments="$(_EmuTarget) emu kill"
56+
/>
57+
</Target>
58+
59+
<!--
60+
<ItemGroup>
61+
<UnitTestApk Include="ApkFile">
62+
<Package></Package>
63+
<InstrumentationType></InstrumentationType>
64+
<ResultsPath></ResultsPath>
65+
</UnitTestApk>
66+
</ItemGroup>
67+
-->
68+
69+
<Target Name="DeployUnitTestApks"
70+
Condition=" '@(UnitTestApk)' != '' ">
71+
<Adb
72+
Arguments="$(_AdbTarget) $(AdbOptions) install -r &quot;%(UnitTestApk.Identity)&quot;"
73+
ToolExe="$(AdbToolExe)"
74+
ToolPath="$(AdbToolPath)"
75+
/>
76+
</Target>
77+
78+
<Target Name="UndeployUnitTestApks"
79+
Condition=" '@(UnitTestApk)' != '' ">
80+
<Adb
81+
Arguments="$(_AdbTarget) $(AdbOptions) uninstall &quot;%(UnitTestApk.Package)&quot;"
82+
ToolExe="$(AdbToolExe)"
83+
ToolPath="$(AdbToolPath)"
84+
/>
85+
</Target>
86+
87+
<Target Name="RunUnitTestApks"
88+
Condition=" '@(UnitTestApk)' != '' ">
89+
<RunInstrumentationTests
90+
AdbTarget="$(_AdbTarget)"
91+
AdbOptions="$(AdbOptions)"
92+
Component="%(UnitTestApk.Package)/%(UnitTestApk.InstrumentationType)"
93+
NUnit2TestResultsFile="%(UnitTestApk.ResultsPath)"
94+
ToolExe="$(AdbToolExe)"
95+
ToolPath="$(AdbToolPath)">
96+
<Output TaskParameter="NUnit2TestResultsFile" PropertyName="_ResultsFile "/>
97+
</RunInstrumentationTests>
98+
</Target>
99+
</Project>

build-tools/xa-prep-tasks/Xamarin.Android.BuildTools.PrepTasks/Git.cs

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
namespace Xamarin.Android.BuildTools.PrepTasks
1414
{
15-
public class Git : ToolTask
15+
public class Git : PathToolTask
1616
{
1717
[Required]
1818
public ITaskItem WorkingDirectory { get; set; }
@@ -26,10 +26,8 @@ protected virtual bool LogTaskMessages {
2626
get { return true; }
2727
}
2828

29-
protected override string ToolName {
30-
get {
31-
return "git" + (Path.DirectorySeparatorChar == '\\' ? ".exe" : "");
32-
}
29+
protected override string ToolBaseName {
30+
get { return "git"; }
3331
}
3432

3533
List<string> lines;
@@ -57,23 +55,6 @@ public override bool Execute ()
5755
return !Log.HasLoggedErrors;
5856
}
5957

60-
protected override string GenerateFullPathToTool ()
61-
{
62-
var app = string.IsNullOrEmpty (ToolExe) ? ToolName: ToolExe;
63-
var path = !string.IsNullOrEmpty (ToolPath) ? ToolPath : GetDirectoryFromPath (app);
64-
return Path.Combine (path, app);
65-
}
66-
67-
string GetDirectoryFromPath (string app)
68-
{
69-
foreach (var p in Environment.GetEnvironmentVariable ("PATH").Split (new [] { Path.PathSeparator.ToString () }, StringSplitOptions.RemoveEmptyEntries)) {
70-
if (File.Exists (Path.Combine (p, app))) {
71-
return p;
72-
}
73-
}
74-
return null;
75-
}
76-
7758
protected override string GenerateCommandLineCommands ()
7859
{
7960
return Arguments;
@@ -86,6 +67,7 @@ protected override string GetWorkingDirectory ()
8667

8768
protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
8869
{
70+
base.LogEventsFromTextOutput (singleLine, messageImportance);
8971
Lines.Add (singleLine);
9072
}
9173
}

0 commit comments

Comments
 (0)