This blog post is a follow-up of Enhancing XcodeGen for simpler maintenance of dependencies in modular iOS app . In the mentioned article I showed you how we enhanced XcodeGen format so only direct dependencies of modules are needed to be specified in project definitions. In this article I would like to present to you a solution that made our PR checks faster, as we run them only for modules that need it. It’s best if you first read the mentioned article to fully understand the issues that we faced. Anyway, I’ll start with a quick recap of the problem.
这篇博客文章是Enhancing XcodeGen的后续文章,用于在模块化iOS应用程序中更轻松地维护依赖关系 。 在提到的文章中,我向您展示了我们如何增强XcodeGen格式,因此仅需要在项目定义中指定模块的直接依赖关系。 在本文中,我想向您介绍一个使PR检查更快的解决方案,因为我们仅对需要它的模块运行它们。 最好先阅读提到的文章,以全面了解我们面临的问题。 无论如何,我将从问题的快速回顾开始。
最初的问题和解决方案 (Initial problem and solution)
In the sample setup we had 3 projects defined with XcodeGen:
在样本设置中,我们用XcodeGen定义了3个项目:
- “ModularApp” application “ ModularApp”应用程序
- “FeatureModule” dynamic framework imported by the application 应用程序导入的“ FeatureModule”动态框架
- “ApiModule” dynamic framework imported by the “Feature” module “功能”模块导入的“ ApiModule”动态框架
The problem that we had was that in the YAML definition of the application project we needed to specify all the dependencies of “Feature” and “ApiModule” frameworks. Similar situation was with the unit tests target of the “Feature” framework — it needed to import all the dependencies of the “ApiModule” framework. The solution that I proposed was a simplified XcodeGen format — thanks to it, we needed only to specify the direct dependencies of every module. We have implemented a python script that analyses all the project definitions in the codebase, adds the indirect dependencies to them and produces a resolved version of the YAML files.
我们遇到的问题是,在应用程序项目的YAML定义中,我们需要指定“功能”和“ ApiModule”框架的所有依赖项。 “功能”框架的单元测试目标也存在类似情况–它需要导入“ ApiModule”框架的所有依赖项。 我提出的解决方案是简化的XcodeGen格式-由于有了它,我们只需要指定每个模块的直接依赖关系即可。 我们已经实现了一个python脚本 ,该脚本可以分析代码库中的所有项目定义,向它们添加间接依赖关系,并生成YAML文件的已解析版本。
As we already traverse through the dependencies tree, as a side effect of the script, we save it and dump it to a JSON file. Thanks to it, we obtained a very handy representation of the dependencies between targets. It’s saved in the repository. You can also see how it looks below:
由于已经遍历了依赖关系树,因此作为脚本的副作用,我们将其保存并转储到JSON文件中。 多亏了它,我们可以很方便地表示目标之间的依赖关系。 它已保存在资源库中 。 您还可以在下面看到它的外观:
{
"ApiModule": [],
"ApiModuleTests": [
"ApiModule"
],
"FeatureModule": [
"ApiModule"
],
"FeatureModuleTests": [
"FeatureModule"
],
"ModularApp": [
"FeatureModule"
],
"ModularAppTests": []
}
可伸缩的拉取请求检查 (Scalable pull request checks)
In our team we always longed for reducing time of our PR checks. As for complex iOS apps running all the unit tests may take a lot of time (like dozens of minutes), we wanted to run tests only for modules which could be affected by changes in the pull request.
在我们的团队中,我们一直渴望减少公关检查的时间。 至于运行所有单元测试的复杂iOS应用程序可能要花费很多时间(例如数十分钟),我们只想对可能受拉请求更改影响的模块运行测试。
Let’s say you modify only code in “ApiModule”. Then it totally makes sense to run only unit tests for “ApiModule” and for “FeatureModule” targets, as the “ApiModule” was affected itself and “FeatureModule” directly depends on the “ApiModule” by importing its public interface. You don’t need to run the unit tests of the “ModularApp” target, as it wasn’t directly affected.
假设您仅修改“ ApiModule”中的代码。 然后,仅针对“ ApiModule”和“ FeatureModule”目标运行单元测试完全有意义,因为“ ApiModule”本身已受到影响,“ FeatureModule”通过导入其公共接口直接依赖于“ ApiModule”。 您无需运行“ ModularApp”目标的单元测试,因为它不会直接受到影响。
As you have seen in my previous article, in my team we really like to improve our work performance using scripts. They proved to be helpful again when working on this challenge. But first let’s see what changes we made to our codebase setup.
正如您在上一篇文章中所看到的,在我们的团队中,我们真的很想使用脚本来提高我们的工作绩效。 在应对这一挑战时,他们再次证明是有帮助的。 但是首先让我们看看我们对代码库设置进行了哪些更改。
Firstly, we needed to make sure that our module project directories exist on the main level of the codebase directory and that they are named the same as projects that are defined in the YAML files. Also each project should have exactly one target (not counting unit/ui test targets) with the same name as the project. This way we ensure that we properly identify changed targets.
首先,我们需要确保模块项目目录位于代码库目录的主目录中,并且它们的名称与YAML文件中定义的项目相同。 同样,每个项目都应该有一个与该项目同名的目标(不计算单元/ ui测试目标)。 这样,我们可以确保正确识别更改的目标。
Secondly, we created a “Tests” project located in the “Tests” directory. Its definition contains references to all our modules. It also defines the “UnitTests” scheme with test targets of all our modules:
其次,我们在“ Tests”目录中创建了一个“ Tests”项目。 它的定义包含对我们所有模块的引用。 它还定义了带有所有模块测试目标的“ UnitTests”方案:
name: Tests
options:
bundleIdPrefix: com.fandom
projectReferences:
ModularApp:
path: ../ModularApp/ModularApp.xcodeproj
FeatureModule:
path: ../FeatureModule/FeatureModule.xcodeproj
ApiModule:
path: ../ApiModule/ApiModule.xcodeproj
schemes:
UnitTests:
build:
targets:
ModularApp/ModularAppTests: [test, run]
FeatureModule/FeatureModuleTests: [test, run]
ApiModule/ApiModuleTests: [test, run]
test:
targets:
- ModularApp/ModularAppTests
- FeatureModule/FeatureModuleTests
- ApiModule/ApiModuleTests
Thirdly, we defined a “Tests” workspace containing references to all our module projects, as well as “Tests” project. We’ll run the tests against it when we find out which unit tests need to be run.
第三,我们定义了一个“测试”工作区,其中包含对我们所有模块项目以及“测试”项目的引用。 当我们发现需要运行哪些单元测试时,我们将对其进行测试。
When we had our setup ready, we created a python script that resolves which unit tests will be run. You can find it here .
准备好设置后,我们创建了一个python脚本来解析将要运行的单元测试。 你可以在这里找到它。
It has few steps.
它只有几个步骤。
- It takes a list of modified projects based on git diff command run against the base branch. It does it by getting first-level directory names of the modified files — that’s why it’s important that projects lie in directories named exactly the same. 它基于针对基本分支运行的git diff命令获取已修改项目的列表。 它通过获取已修改文件的一级目录名称来做到这一点,这就是为什么将项目放置在完全相同的目录中很重要的原因。
We load the “Tests” project YAML definition. Then we iterate over test targets defined there and compare it with the list of modified projects.
我们加载“测试”项目的YAML定义。 然后,我们遍历在那里定义的测试目标,并将其与修改后的项目列表进行比较。
- We check if the project containing the test target belongs to a list of modified projects
-我们检查包含测试目标的项目是否属于已修改项目的列表
- We check if any of the project dynamic framework target’s dependencies belongs to a list of modified projects
-我们检查项目动态框架目标的任何依赖项是否属于已修改项目的列表
- We check if any of the test target’s dependencies belongs to a list of modified projects
-我们检查测试目标的任何依赖项是否属于已修改项目的列表
- If none of the above conditions are met, the test target is removed from the YAML definition of the “Tests” project. 如果以上条件都不满足,则将测试目标从“测试”项目的YAML定义中删除。
- The new YAML definition is saved as a project-resolved.yml file, which will be used to generate Tests.xcodeproj. 新的YAML定义另存为project-resolved.yml文件,该文件将用于生成Tests.xcodeproj。
- It calls XcodeGen to generate the Tests.xcodeproj 它调用XcodeGen生成Tests.xcodeproj
When we have our project generated, we just need to open the Tests.xcworkspace and run the “UnitTests” scheme.
生成项目后,我们只需要打开Tests.xcworkspace并运行“ UnitTests”方案即可。
结论 (Conclusions)
This solution gives us measurable benefits if it comes to PR checks waiting time. Running all our unit tests currently takes about 15 minutes. With this solution, a PR check takes on average about 6.8 minutes. Of course there are still situations where we need to wait a lot of time when we modify a widely used module (it’s hard not to fall into the “Helpers” module trap imported all over your application). However, we are making steps to reduce the amount of code that needs to be put in such modules. Another way to improve the solution is to detect the changes made only to “public” interfaces of the modules and run unit tests on their dependencies only when such code is modified. However you need to consider how often you make non-public changes in an application consisting of multiple modules and whether the profit will be greater than the cost if introducing such a solution.
如果涉及PR检查等待时间,此解决方案可为我们带来可观的收益。 目前运行所有单元测试大约需要15分钟。 使用此解决方案,PR检查平均大约需要6.8分钟。 当然,在某些情况下,修改广泛使用的模块时我们需要等待很多时间(很难不落入整个应用程序中导入的“ Helpers”模块陷阱中)。 但是,我们正在采取措施减少此类模块中需要放入的代码量。 改进解决方案的另一种方法是仅检测对模块的“公共”接口所做的更改,并仅在修改此类代码后对其依赖性进行单元测试。 但是,您需要考虑在由多个模块组成的应用程序中进行不公开更改的频率,以及引入这种解决方案后利润是否大于成本。
Originally published at https://dev.fandom.com.
最初发布在 https://dev.fandom.com 。
翻译自: https://medium.com/fandom-engineering/faster-pull-request-checks-for-modular-ios-app-ff46d3e0bedc