Open projects using the packaged language server (#7868)

close #7750
close #7834

Changelog:
- update: project manager uses the packaged language server to open projects
- fix: remove stack traces from connection errors on initial ping handler request (when the language server is booting)
- update: add engine and edition versions to the `initProtocolConnection` response for easier debug
- update: do not resolve project ensoVersion in the `project/list` to eliminate unnecessary network calls
This commit is contained in:
Dmitry Bushev 2023-09-28 15:13:14 +01:00 committed by GitHub
parent ffa036411d
commit 71442fe32c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 132 additions and 147 deletions

View File

@ -172,15 +172,13 @@ impl From<ProjectNormalizedName> for ImString {
#[serde(rename_all = "camelCase")]
pub struct ProjectMetadata {
/// Project's name.
pub name: ProjectName,
pub name: ProjectName,
/// Project's namespace,
pub namespace: String,
pub namespace: String,
/// Project's uuid.
pub id: Uuid,
/// Engine version to use for the project, represented by a semver version string.
pub engine_version: Option<String>,
pub id: Uuid,
/// Last time the project was opened.
pub last_opened: Option<UTCDateTime>,
pub last_opened: Option<UTCDateTime>,
}
/// This type specifies what action should be taken if an Engine's component required to complete
@ -319,33 +317,29 @@ mod mock_client_tests {
fn list_projects() {
let mock_client = MockClient::default();
let project1 = ProjectMetadata {
name: ProjectName::new_unchecked("project1"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2020-01-07T21:25:26Z").unwrap()),
engine_version: Some("0.2.21".to_owned()),
namespace: "local".to_owned(),
name: ProjectName::new_unchecked("project1"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2020-01-07T21:25:26Z").unwrap()),
namespace: "local".to_owned(),
};
let project2 = ProjectMetadata {
name: ProjectName::new_unchecked("project2"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2020-02-02T13:15:20Z").unwrap()),
engine_version: Some("0.2.22".to_owned()),
namespace: "local".to_owned(),
name: ProjectName::new_unchecked("project2"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2020-02-02T13:15:20Z").unwrap()),
namespace: "local".to_owned(),
};
let expected_recent_projects = response::ProjectList { projects: vec![project1, project2] };
let sample1 = ProjectMetadata {
name: ProjectName::new_unchecked("sample1"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2019-11-23T05:30:12Z").unwrap()),
engine_version: Some("0.2.21".to_owned()),
namespace: "test".to_owned(),
name: ProjectName::new_unchecked("sample1"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2019-11-23T05:30:12Z").unwrap()),
namespace: "test".to_owned(),
};
let sample2 = ProjectMetadata {
name: ProjectName::new_unchecked("sample2"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2019-12-25T00:10:58Z").unwrap()),
engine_version: Some("0.2.21".to_owned()),
namespace: "test".to_owned(),
name: ProjectName::new_unchecked("sample2"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2019-12-25T00:10:58Z").unwrap()),
namespace: "test".to_owned(),
};
let expected_sample_projects = response::ProjectList { projects: vec![sample1, sample2] };
expect_call!(mock_client.list_projects(count=Some(2)) =>
@ -483,18 +477,16 @@ mod remote_client_tests {
let number_of_projects_json = json!({ "numberOfProjects": number_of_projects });
let num_projects_json = json!({ "numProjects": number_of_projects });
let project1 = ProjectMetadata {
name: ProjectName::new_unchecked("project1"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2020-01-07T21:25:26Z").unwrap()),
engine_version: Some("0.2.21".to_owned()),
namespace: "local".to_owned(),
name: ProjectName::new_unchecked("project1"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2020-01-07T21:25:26Z").unwrap()),
namespace: "local".to_owned(),
};
let project2 = ProjectMetadata {
name: ProjectName::new_unchecked("project2"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2020-02-02T13:15:20Z").unwrap()),
engine_version: Some("0.2.22".to_owned()),
namespace: "local".to_owned(),
name: ProjectName::new_unchecked("project2"),
id: Uuid::default(),
last_opened: Some(DateTime::parse_from_rfc3339("2020-02-02T13:15:20Z").unwrap()),
namespace: "local".to_owned(),
};
let project_list = response::ProjectList { projects: vec![project1, project2] };
let project_list_json = json!({
@ -503,14 +495,12 @@ mod remote_client_tests {
"id" : "00000000-0000-0000-0000-000000000000",
"lastOpened" : "2020-01-07T21:25:26+00:00",
"name" : "project1",
"engineVersion" : "0.2.21",
"namespace" : "local"
},
{
"id" : "00000000-0000-0000-0000-000000000000",
"lastOpened" : "2020-02-02T13:15:20+00:00",
"name" : "project2",
"engineVersion" : "0.2.22",
"namespace" : "local"
}
]

View File

@ -298,11 +298,10 @@ mod test {
let mock_client = project_manager::MockClient::default();
let project_name = ProjectName::new_unchecked("TestProject");
let project = project_manager::ProjectMetadata {
name: project_name.clone(),
id: uuid::Uuid::new_v4(),
last_opened: default(),
engine_version: Some("127.0.01".to_owned()),
namespace: "local".to_owned(),
name: project_name.clone(),
id: Uuid::new_v4(),
last_opened: default(),
namespace: "local".to_owned(),
};
let expected_id = project.id;
let projects = vec![project];

View File

@ -259,7 +259,11 @@ class JsonConnectionController(
receiver ! ResponseResult(
InitProtocolConnection,
request.id,
InitProtocolConnection.Result(allRoots.map(_.toContentRoot).toSet)
InitProtocolConnection.Result(
buildinfo.Info.ensoVersion,
buildinfo.Info.currentEdition,
allRoots.map(_.toContentRoot).toSet
)
)
initialize(webActor, rpcSession)

View File

@ -16,7 +16,11 @@ object SessionApi {
case class Params(clientId: UUID)
case class Result(contentRoots: Set[ContentRoot])
case class Result(
ensoVersion: String,
currentEdition: String,
contentRoots: Set[ContentRoot]
)
implicit
val hasParams: HasParams.Aux[this.type, InitProtocolConnection.Params] =

View File

@ -82,7 +82,7 @@ object GlobalConfigurationManager {
val globalConfigName: String = "global-config.yaml"
/** Tries to read the global config from the given `path`. */
def readConfig(path: Path): Try[GlobalConfig] =
private def readConfig(path: Path): Try[GlobalConfig] =
Using(Files.newBufferedReader(path)) { reader =>
for {
json <- yaml.parser.parse(reader)
@ -91,7 +91,7 @@ object GlobalConfigurationManager {
}.flatMap(_.toTry)
/** Tries to write the provided `config` to the given `path`. */
def writeConfig(path: Path, config: GlobalConfig): Try[Unit] =
private def writeConfig(path: Path, config: GlobalConfig): Try[Unit] =
writeConfigRaw(path, GlobalConfig.encoder(config))
/** Tries to write the config from a raw JSON value to the given `path`.
@ -99,7 +99,7 @@ object GlobalConfigurationManager {
* The config will not be saved if it is invalid, instead an exception is
* thrown.
*/
def writeConfigRaw(path: Path, rawConfig: Json): Try[Unit] = {
private def writeConfigRaw(path: Path, rawConfig: Json): Try[Unit] = {
def verifyConfig: Try[Unit] =
rawConfig.as[GlobalConfig] match {
case Left(failure) =>

View File

@ -3,15 +3,11 @@ package org.enso.projectmanager.data
import java.time.OffsetDateTime
import java.util.UUID
import nl.gn0s1s.bump.SemVer
/** Contains project metadata.
*
* @param name the name of the project
* @param namespace the namespace of the project
* @param id the project id
* @param engineVersion version of the engine associated with the project, it
* may be missing if project's edition could not be loaded
* @param created the project creation time
* @param lastOpened the last opened datetime
*/
@ -19,7 +15,6 @@ case class ProjectMetadata(
name: String,
namespace: String,
id: UUID,
engineVersion: Option[SemVer],
created: OffsetDateTime,
lastOpened: Option[OffsetDateTime]
)

View File

@ -169,13 +169,13 @@ class HeartbeatSession(
if (quietErrors) {
logger.debug(s"$message ($throwable)", arg)
} else {
logger.error(s"$message {}", arg, throwable.getMessage)
logger.error(s"$message {}", arg, throwable)
}
}
private def logError(throwable: Throwable, message: String): Unit = {
if (quietErrors) {
logger.debug(s"$message ($throwable)")
logger.debug(s"$message ({})", throwable.getMessage)
} else {
logger.error(message, throwable)
}

View File

@ -76,7 +76,7 @@ object ProjectCreateHandler {
/** Creates a configuration object used to create a [[ProjectCreateHandler]].
*
* @param configurationService
* @param configurationService a global configuration service
* @param projectService a project service
* @param requestTimeout a request timeout
* @param timeoutRetries a number of timeouts to wait until a failure is reported

View File

@ -8,11 +8,7 @@ import org.enso.editions.DefaultEdition
import org.enso.pkg.Config
import org.enso.pkg.validation.NameValidation
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.core.{
Applicative,
CovariantFlatMap,
Traverse
}
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.data.{
@ -44,6 +40,7 @@ import org.enso.projectmanager.service.validation.ProjectNameValidator
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.CurrentVersion
import java.util.UUID
@ -58,7 +55,7 @@ import java.util.UUID
* @param gen a random generator
*/
class ProjectService[
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap: Applicative
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap
](
validator: ProjectNameValidator[F],
repo: ProjectRepository[F],
@ -280,10 +277,12 @@ class ProjectService[
openTime <- clock.nowInUtc()
updated = project.copy(lastOpened = Some(openTime))
_ <- repo.update(updated).mapError(toServiceFailure)
projectWithDefaultEdition =
updated.copy(edition = Some(DefaultEdition.getDefaultEdition))
sockets <- startServer(
progressTracker,
clientId,
updated,
projectWithDefaultEdition,
missingComponentAction
)
} yield sockets
@ -315,6 +314,7 @@ class ProjectService[
project: Project,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, RunningLanguageServerInfo] = for {
_ <- log.debug("Preparing to start the Language Server for [{}].", project)
version <- resolveProjectVersion(project)
_ <- preinstallEngine(progressTracker, version, missingComponentAction)
sockets <- languageServerGateway
@ -322,17 +322,14 @@ class ProjectService[
.mapError {
case PreviousInstanceNotShutDown =>
ProjectOpenFailed(
"The previous instance of the Language Server hasn't been shut " +
"down yet."
"The previous instance of the Language Server hasn't been shut down yet."
)
case ServerBootTimedOut =>
ProjectOpenFailed("Language server boot timed out.")
case ServerBootFailed(th) =>
ProjectOpenFailed(
s"Language server boot failed. ${th.getMessage}"
)
ProjectOpenFailed(s"Language server boot failed. ${th.getMessage}")
}
} yield RunningLanguageServerInfo(
version,
@ -369,36 +366,15 @@ class ProjectService[
.take(maybeSize.getOrElse(Int.MaxValue))
)
.mapError(toServiceFailure)
.flatMap(xs => Traverse[List].traverse(xs)(resolveProjectMetadata))
.map(_.map(toProjectMetadata))
private def resolveProjectMetadata(
project: Project
): F[ProjectServiceFailure, ProjectMetadata] = {
val version = resolveProjectVersion(project)
for {
version <- version.map(Some(_)).recover { error =>
// TODO [RW] We may consider sending this warning to the IDE once
// a warning protocol is implemented (#1860).
logger.warn(
s"Could not resolve engine version for project ${project.name}: " +
s"$error"
)
None
}
} yield toProjectMetadata(version, project)
}
private def toProjectMetadata(
engineVersion: Option[SemVer],
project: Project
): ProjectMetadata =
private def toProjectMetadata(project: Project): ProjectMetadata =
ProjectMetadata(
name = project.name,
namespace = project.namespace,
id = project.id,
engineVersion = engineVersion,
created = project.created,
lastOpened = project.lastOpened
name = project.name,
namespace = project.namespace,
id = project.id,
created = project.created,
lastOpened = project.lastOpened
)
private def getUserProject(
@ -464,31 +440,36 @@ class ProjectService[
private def resolveProjectVersion(
project: Project
): F[ProjectServiceFailure, SemVer] =
Sync[F]
.blockingOp {
// TODO [RW] at some point we will need to use the configuration service to get the actual default version, see #1864
val _ = configurationService
if (project.edition.contains(DefaultEdition.getDefaultEdition)) {
CovariantFlatMap[F].pure(CurrentVersion.version)
} else {
Sync[F]
.blockingOp {
// TODO [RW] at some point we will need to use the configuration service to get the actual default version, see #1864
val _ = configurationService
val edition =
project.edition.getOrElse(DefaultEdition.getDefaultEdition)
val edition =
project.edition.getOrElse(DefaultEdition.getDefaultEdition)
distributionConfiguration.editionManager
.resolveEngineVersion(edition)
.orElse {
logger.warn(
s"Could not resolve engine version for ${edition}. Falling " +
s"back to ${DefaultEdition.getDefaultEdition}"
)
distributionConfiguration.editionManager
.resolveEngineVersion(DefaultEdition.getDefaultEdition)
}
.get
}
.mapError { error =>
ProjectServiceFailure.GlobalConfigurationAccessFailure(
s"Could not resolve project engine version: ${error.getMessage}"
)
}
distributionConfiguration.editionManager
.resolveEngineVersion(edition)
.orElse {
logger.warn(
s"Could not resolve engine version for [{}]. Falling back to [{}].",
edition,
DefaultEdition.getDefaultEdition
)
distributionConfiguration.editionManager
.resolveEngineVersion(DefaultEdition.getDefaultEdition)
}
.get
}
.mapError { error =>
ProjectServiceFailure.GlobalConfigurationAccessFailure(
s"Could not resolve project engine version: ${error.getMessage}"
)
}
}
private def getNameForNewProject(
projectName: String,

View File

@ -43,6 +43,7 @@ import org.enso.projectmanager.service.versionmanagement.{
}
import org.enso.projectmanager.service.{ProjectCreationService, ProjectService}
import org.enso.projectmanager.test.{ObservableGenerator, ProgrammableClock}
import org.enso.runtimeversionmanager.CurrentVersion
import org.enso.runtimeversionmanager.components.GraalVMVersion
import org.enso.runtimeversionmanager.test.FakeReleases
import org.scalatest.BeforeAndAfterAll
@ -248,9 +249,8 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll {
}
private def setupEditions(): Unit = {
val engineVersion =
engineToInstall.map(_.toString).getOrElse(buildinfo.Info.ensoVersion)
val editionsDir = testDistributionRoot.toPath / "test_data" / "editions"
val engineVersion = engineToInstall.getOrElse(CurrentVersion.version)
val editionsDir = testDistributionRoot.toPath / "test_data" / "editions"
Files.createDirectories(editionsDir)
val editionName = buildinfo.Info.currentEdition + ".yaml"
val editionConfig =

View File

@ -1,26 +1,27 @@
package org.enso.projectmanager.infrastructure.languageserver
import akka.testkit.TestDuration
import nl.gn0s1s.bump.SemVer
import io.circe.literal._
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.test.Net._
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.testkit.{FlakySpec, RetrySpec}
import org.enso.runtimeversionmanager.test.OverrideTestVersionSuite
import scala.concurrent.Await
import scala.concurrent.duration._
class LanguageServerGatewaySpec
extends BaseServerSpec
with FlakySpec
with ProjectManagementOps
with RetrySpec {
with OverrideTestVersionSuite
with ProjectManagementOps {
override val testVersion: SemVer = SemVer(0, 0, 1)
override val engineToInstall = Some(SemVer(0, 0, 1))
"A language server service" must {
"kill all running language servers" taggedAs Retry ignore {
"kill all running language servers" ignore {
implicit val client = new WsTestClient(address)
val fooId = createProject("foo")
val barId = createProject("bar")

View File

@ -11,7 +11,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
"engine/*" must {
"report no installed engines by default" in {
implicit val client = new WsTestClient(address)
implicit val client: WsTestClient = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "engine/list-installed",

View File

@ -42,6 +42,7 @@ trait MissingComponentBehavior {
"fail if the requested missing version is marked as broken with " +
"Install" in {
pending // #7750
val client = new WsTestClient(address)
client.send(buildRequest(brokenVersion, MissingComponentAction.Install))
client.expectError(4021)

View File

@ -4,22 +4,27 @@ import akka.testkit.TestDuration
import io.circe.literal._
import nl.gn0s1s.bump.SemVer
import org.apache.commons.io.FileUtils
import org.enso.editions.SemVerJson._
import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.runtimeversionmanager.CurrentVersion
import org.enso.runtimeversionmanager.test.OverrideTestVersionSuite
import org.enso.testkit.FlakySpec
import java.io.File
import java.nio.file.{Files, Paths}
import java.util.UUID
import scala.concurrent.duration._
import scala.io.Source
class ProjectManagementApiSpec
extends BaseServerSpec
with FlakySpec
with OverrideTestVersionSuite
with ProjectManagementOps {
override val testVersion: SemVer = SemVer(0, 0, 1)
override def beforeEach(): Unit = {
super.beforeEach()
gen.reset()
@ -295,7 +300,7 @@ class ProjectManagementApiSpec
"id": 1,
"params": {
"name": "Foo",
"version": "0.0.1"
"version": ${CurrentVersion.version.toString()}
}
}
""")
@ -445,7 +450,7 @@ class ProjectManagementApiSpec
""")
val result = openProjectData
result.projectName shouldEqual projectName
result.engineVersion shouldEqual SemVer("0.0.1").get
result.engineVersion shouldEqual CurrentVersion.version
// teardown
closeProject(projectId)
@ -660,7 +665,6 @@ class ProjectManagementApiSpec
"name": "Foo",
"namespace": "local",
"id": $projectId1,
"engineVersion": $engineToInstall,
"created": $projectCreationTime,
"lastOpened": $projectOpenTime
},
@ -668,7 +672,6 @@ class ProjectManagementApiSpec
"name": "Foo",
"namespace": "local",
"id": $projectId2,
"engineVersion": $engineToInstall,
"created": $projectCreationTime,
"lastOpened": null
}
@ -773,7 +776,6 @@ class ProjectManagementApiSpec
"name": "Baz",
"namespace": "local",
"id": $bazId,
"engineVersion": $engineToInstall,
"created": $projectBazCreationTime,
"lastOpened": null
},
@ -781,7 +783,6 @@ class ProjectManagementApiSpec
"name": "Bar",
"namespace": "local",
"id": $barId,
"engineVersion": $engineToInstall,
"created": $projectBarCreationTime,
"lastOpened": null
},
@ -789,7 +790,6 @@ class ProjectManagementApiSpec
"name": "Foo",
"namespace": "local",
"id": $fooId,
"engineVersion": $engineToInstall,
"created": $projectFooCreationTime,
"lastOpened": null
}
@ -836,7 +836,6 @@ class ProjectManagementApiSpec
"name": "Bar",
"namespace": "local",
"id": $barId,
"engineVersion": $engineToInstall,
"created": $projectBarCreationTime,
"lastOpened": null
},
@ -844,7 +843,6 @@ class ProjectManagementApiSpec
"name": "Foo",
"namespace": "local",
"id": $fooId,
"engineVersion": $engineToInstall,
"created": $projectFooCreationTime,
"lastOpened": null
}
@ -895,7 +893,6 @@ class ProjectManagementApiSpec
"name": "Quux",
"namespace": "local",
"id": $quuxId,
"engineVersion": $engineToInstall,
"created": $projectQuuxCreationTime,
"lastOpened": null
},
@ -903,7 +900,6 @@ class ProjectManagementApiSpec
"name": "Baz",
"namespace": "local",
"id": $bazId,
"engineVersion": $engineToInstall,
"created": $creationTime,
"lastOpened": $bazOpenTime
},
@ -911,7 +907,6 @@ class ProjectManagementApiSpec
"name": "Bar",
"namespace": "local",
"id": $barId,
"engineVersion": $engineToInstall,
"created": $creationTime,
"lastOpened": $barOpenTime
},
@ -919,7 +914,6 @@ class ProjectManagementApiSpec
"name": "Foo",
"namespace": "local",
"id": $fooId,
"engineVersion": $engineToInstall,
"created": $creationTime,
"lastOpened": null
}
@ -968,7 +962,6 @@ class ProjectManagementApiSpec
"name": "Foo",
"namespace": "local",
"id": $projectId1,
"engineVersion": $engineToInstall,
"created": $creationTime,
"lastOpened": null
},
@ -976,7 +969,6 @@ class ProjectManagementApiSpec
"name": "Foo",
"namespace": "local",
"id": $projectId2,
"engineVersion": $engineToInstall,
"created": $creationTime,
"lastOpened": null
}

View File

@ -1,6 +1,12 @@
package org.enso.projectmanager.protocol
import nl.gn0s1s.bump.SemVer
import org.enso.runtimeversionmanager.test.OverrideTestVersionSuite
class ProjectOpenHandleMissingRuntimeSpec extends ProjectOpenSpecBase {
class ProjectOpenHandleMissingRuntimeSpec
extends ProjectOpenSpecBase
with OverrideTestVersionSuite {
override def testVersion: SemVer = SemVer(0, 0, 1)
"project/open" should {
behave like correctlyHandleMissingRuntimeInPresenceOfEngine()

View File

@ -1,6 +1,12 @@
package org.enso.projectmanager.protocol
import nl.gn0s1s.bump.SemVer
import org.enso.runtimeversionmanager.test.OverrideTestVersionSuite
class ProjectOpenMissingComponentsSpec extends ProjectOpenSpecBase {
class ProjectOpenMissingComponentsSpec
extends ProjectOpenSpecBase
with OverrideTestVersionSuite {
override val testVersion: SemVer = defaultVersion
override def beforeAll(): Unit = {
super.beforeAll()
@ -10,4 +16,5 @@ class ProjectOpenMissingComponentsSpec extends ProjectOpenSpecBase {
"project/open" should {
behave like correctlyHandleMissingComponents()
}
}

View File

@ -10,6 +10,7 @@ import zio.{ZAny, ZIO}
import java.util.UUID
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.runtimeversionmanager.test.OverrideTestVersionSuite
import org.enso.testkit.FlakySpec
import scala.concurrent.duration._
@ -17,8 +18,11 @@ import scala.concurrent.duration._
class ProjectShutdownSpec
extends BaseServerSpec
with FlakySpec
with OverrideTestVersionSuite
with ProjectManagementOps {
override val testVersion: SemVer = SemVer(0, 0, 1)
override def beforeEach(): Unit = {
super.beforeEach()
gen.reset()