From f676f236b1f0aaa77576b7d9781d866ade2602fd Mon Sep 17 00:00:00 2001 From: mdnapo Date: Sat, 5 Oct 2024 13:37:14 +0200 Subject: [PATCH] Added CLI test project and tests for 'm4g init' command --- .dockerignore | 19 ++ Dockerfile | 30 +++ .../ContainerInterfaceExtensions.cs | 11 + MycroForge.CLI.Tests/GlobalUsings.cs | 1 + MycroForge.CLI.Tests/InitCommandTests.cs | 222 ++++++++++++++++++ .../MycroForge.CLI.Tests.csproj | 25 ++ MycroForge.CLI/scripts/publish-tool.sh | 2 +- .../DefaultJsonSerializerOptions.cs | 17 ++ .../Extensions/ObjectStreamExtensions.cs | 9 +- MycroForge.Core/ProjectContext.cs | 6 +- MycroForge.Core/Serialization.cs | 20 -- MycroForge.sln | 6 + 12 files changed, 338 insertions(+), 30 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 MycroForge.CLI.Tests/Extensions/ContainerInterfaceExtensions.cs create mode 100644 MycroForge.CLI.Tests/GlobalUsings.cs create mode 100644 MycroForge.CLI.Tests/InitCommandTests.cs create mode 100644 MycroForge.CLI.Tests/MycroForge.CLI.Tests.csproj create mode 100644 MycroForge.Core/DefaultJsonSerializerOptions.cs delete mode 100644 MycroForge.Core/Serialization.cs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f61d55b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +# directories +**/bin/ +**/obj/ +**/out/ +.git +.idea +docs +.gitignore +.dockerignore +MycroForge.sln.DotSettings.user +nuget.docker-compose.yml +README.md + +# files +Dockerfile* +**/*.md + +#MycroForge.PluginTemplate +#MycroForge.PluginTemplate.Package \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6748207 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim + +# Copy the files to the /tool directory. +WORKDIR /tool +COPY . . +# Manually add a reference to MycroForge.Core in the MycroForge.PluginTemplate project, +# otherwise it will try to fetch it from a nuget repository. +RUN dotnet add MycroForge.PluginTemplate reference MycroForge.Core + +# Install the tools required for testing +RUN apt update -y && \ + apt upgrade -y && \ + apt install -y git && \ + apt install -y bash && \ + apt install -y python3 && \ + apt install -y python3-pip && \ + apt install -y python3-venv + +# Publish the CLI as a global tool and add the .dotnet/tools folder the the PATH variable. +WORKDIR /tool/MycroForge.CLI +RUN ./scripts/publish-tool.sh +ENV PATH="$PATH:/root/.dotnet/tools" + +WORKDIR /test + +SHELL ["/bin/bash", "-c"] + +ENV PATH="$PATH:/root/.dotnet/tools" + +CMD ["sleep", "infinity"] diff --git a/MycroForge.CLI.Tests/Extensions/ContainerInterfaceExtensions.cs b/MycroForge.CLI.Tests/Extensions/ContainerInterfaceExtensions.cs new file mode 100644 index 0000000..c0ef361 --- /dev/null +++ b/MycroForge.CLI.Tests/Extensions/ContainerInterfaceExtensions.cs @@ -0,0 +1,11 @@ +using DotNet.Testcontainers.Containers; + +namespace MycroForge.CLI.Tests.Extensions; + +internal static class ContainerInterfaceExtensions +{ + public static Task ReadFileFromRootAsync(this IContainer container, string file) + { + return container.ReadFileAsync($"/test/todo/{file}"); + } +} \ No newline at end of file diff --git a/MycroForge.CLI.Tests/GlobalUsings.cs b/MycroForge.CLI.Tests/GlobalUsings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/MycroForge.CLI.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/MycroForge.CLI.Tests/InitCommandTests.cs b/MycroForge.CLI.Tests/InitCommandTests.cs new file mode 100644 index 0000000..75b7730 --- /dev/null +++ b/MycroForge.CLI.Tests/InitCommandTests.cs @@ -0,0 +1,222 @@ +using System.Text; +using System.Text.Json; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Images; +using MycroForge.CLI.Tests.Extensions; +using MycroForge.Core; + +namespace MycroForge.CLI.Tests; + +public class InitCommandTests +{ + private IFutureDockerImage Image = null!; + private IContainer Container = null!; + + private async Task CreateImage() + { + Image = new ImageFromDockerfileBuilder() + .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), string.Empty) + .WithDockerfile("Dockerfile") + .WithCleanUp(true) + .Build(); + + await Image.CreateAsync(); + } + + private async Task CreateContainer() + { + Container = new ContainerBuilder() + .WithImage(Image) + .WithCleanUp(true) + .Build(); + + await Container.StartAsync(); + } + + [OneTimeSetUp] + public async Task SetupOnce() + { + await CreateImage(); + + await CreateContainer(); + + await Container.ExecAsync(["m4g", "init", "todo"]); + } + + [Test] + public async Task Creates_m4g_json() + { + var bytes = await Container.ReadFileFromRootAsync("m4g.json"); + using var stream = new MemoryStream(bytes); + var config = await JsonSerializer.DeserializeAsync(stream, DefaultJsonSerializerOptions.Default); + + Assert.That(config, Is.Not.Null); + + Assert.Multiple(() => + { + Assert.That(config!.Api, Is.Not.Null); + Assert.That(config.Api.Port, Is.EqualTo(8000)); + Assert.That(config.Db, Is.Not.Null); + Assert.That(config.Db.DbhPort, Is.EqualTo(5050)); + Assert.That(config.Db.DbuPort, Is.EqualTo(5051)); + }); + } + + [Test] + public async Task Creates_main_py() + { + var bytes = await Container.ReadFileFromRootAsync("main.py"); + var text = Encoding.UTF8.GetString(bytes); + var lines = text.Split('\n'); + + TestContext.WriteLine(text); + + Assert.Multiple(() => + { + Assert.That(text, Is.Not.Empty); + Assert.That(lines, Does.Contain("from fastapi import FastAPI")); + Assert.That(lines, Does.Contain("from api.routers import hello")); + Assert.That(lines, Does.Contain("app = FastAPI()")); + Assert.That(lines, Does.Contain("app.include_router(prefix=\"/hello\", router=hello.router)")); + }); + } + + [Test] + public async Task Creates_gitignore() + { + var bytes = await Container.ReadFileFromRootAsync(".gitignore"); + var text = Encoding.UTF8.GetString(bytes); + var lines = text.Split('\n'); + + TestContext.WriteLine(text); + + Assert.Multiple(() => + { + Assert.That(text, Is.Not.Empty); + Assert.That(lines, Does.Contain(".venv")); + Assert.That(lines, Does.Contain("__pycache__/")); + }); + } + + [Test] + public async Task Creates_alembic_ini() + { + var bytes = await Container.ReadFileFromRootAsync("alembic.ini"); + var text = Encoding.UTF8.GetString(bytes); + var lines = text.Split('\n'); + + TestContext.WriteLine(text); + + Assert.Multiple(() => + { + Assert.That(text, Is.Not.Empty); + Assert.That(lines, Does.Contain("[alembic]")); + Assert.That(lines, Does.Contain("sqlalchemy.url = driver://user:pass@localhost/dbname")); + }); + } + + [Test] + public async Task Creates_db_docker_compose_yml() + { + var bytes = await Container.ReadFileFromRootAsync("db.docker-compose.yml"); + var text = Encoding.UTF8.GetString(bytes); + var lines = text.Split('\n'); + + TestContext.WriteLine(text); + + Assert.Multiple(() => + { + Assert.That(text, Is.Not.Empty); + Assert.That(lines, Does.Contain(" todo_mariadb:")); + Assert.That(lines, Does.Contain(" container_name: 'todo_mariadb'")); + Assert.That(lines, Does.Contain(" - '${DBH_PORT}:3306'")); + Assert.That(lines, Does.Contain(" MYSQL_DATABASE: 'todo'")); + Assert.That(lines, Does.Contain(" - 'todo_mariadb:/var/lib/mysql'")); + Assert.That(lines, Does.Contain(" todo_phpmyadmin:")); + Assert.That(lines, Does.Contain(" container_name: todo_phpmyadmin")); + Assert.That(lines, Does.Contain(" PMA_HOST: todo_mariadb")); + Assert.That(lines, Does.Contain(" - todo_mariadb")); + Assert.That(lines, Does.Contain(" todo_mariadb:")); + }); + } + + [Test] + public async Task Creates_api__router_hello_py() + { + var bytes = await Container.ReadFileFromRootAsync("api/routers/hello.py"); + var text = Encoding.UTF8.GetString(bytes); + var lines = text.Split('\n'); + + TestContext.WriteLine(text); + + Assert.Multiple(() => + { + Assert.That(text, Is.Not.Empty); + Assert.That(lines, Does.Contain("from fastapi import APIRouter")); + Assert.That(lines, Does.Contain("from fastapi.responses import JSONResponse")); + Assert.That(lines, Does.Contain("from fastapi.encoders import jsonable_encoder")); + Assert.That(lines, Does.Contain("router = APIRouter()")); + Assert.That(lines, Does.Contain("@router.get(\"/{name}\")")); + Assert.That(lines, Does.Contain("async def hello(name: str):")); + Assert.That(lines, Does.Contain("\treturn JSONResponse(status_code=200, content=jsonable_encoder({'greeting': f\"Hello, {name}!\"}))")); + }); + } + + [Test] + public async Task Creates_db__engine__async_session_py() + { + var bytes = await Container.ReadFileFromRootAsync("db/engine/async_session.py"); + var text = Encoding.UTF8.GetString(bytes); + var lines = text.Split('\n'); + + TestContext.WriteLine(text); + + Assert.Multiple(() => + { + Assert.That(text, Is.Not.Empty); + Assert.That(lines, Does.Contain("from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, AsyncSession")); + Assert.That(lines, Does.Contain("from db.settings import DbSettings")); + Assert.That(lines, Does.Contain("async_engine: AsyncEngine = create_async_engine(DbSettings.get_connectionstring())")); + Assert.That(lines, Does.Contain("def async_session() -> AsyncSession:")); + Assert.That(lines, Does.Contain("\treturn AsyncSession(async_engine, expire_on_commit=False)")); + }); + } + + [Test] + public async Task Creates_db__entities__entity_base_py() + { + var bytes = await Container.ReadFileFromRootAsync("db/entities/entity_base.py"); + var text = Encoding.UTF8.GetString(bytes); + var lines = text.Split('\n'); + + TestContext.WriteLine(text); + + Assert.Multiple(() => + { + Assert.That(text, Is.Not.Empty); + Assert.That(lines, Does.Contain("from sqlalchemy.orm import DeclarativeBase")); + Assert.That(lines, Does.Contain("class EntityBase(DeclarativeBase):")); + Assert.That(lines, Does.Contain("\tpass")); + }); + } + + [Test] + public async Task Creates_db__settings_py() + { + var bytes = await Container.ReadFileFromRootAsync("db/settings.py"); + var text = Encoding.UTF8.GetString(bytes); + var lines = text.Split('\n'); + + TestContext.WriteLine(text); + + Assert.Multiple(() => + { + Assert.That(text, Is.Not.Empty); + Assert.That(lines, Does.Contain("class DbSettings:")); + Assert.That(lines, Does.Contain("\tdef get_connectionstring() -> str:")); + Assert.That(lines, Does.Contain("\t\tconnectionstring = \"mysql+asyncmy://root:password@localhost:5050/todo\"")); + Assert.That(lines, Does.Contain("\t\treturn connectionstring")); + }); + } +} diff --git a/MycroForge.CLI.Tests/MycroForge.CLI.Tests.csproj b/MycroForge.CLI.Tests/MycroForge.CLI.Tests.csproj new file mode 100644 index 0000000..3501fb2 --- /dev/null +++ b/MycroForge.CLI.Tests/MycroForge.CLI.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/MycroForge.CLI/scripts/publish-tool.sh b/MycroForge.CLI/scripts/publish-tool.sh index 57a756f..09a131e 100644 --- a/MycroForge.CLI/scripts/publish-tool.sh +++ b/MycroForge.CLI/scripts/publish-tool.sh @@ -1,4 +1,4 @@ -#!/usr/bin/bash +#!/bin/bash dotnet pack -v d diff --git a/MycroForge.Core/DefaultJsonSerializerOptions.cs b/MycroForge.Core/DefaultJsonSerializerOptions.cs new file mode 100644 index 0000000..4f774a4 --- /dev/null +++ b/MycroForge.Core/DefaultJsonSerializerOptions.cs @@ -0,0 +1,17 @@ +using System.Text.Json; + +namespace MycroForge.Core; + +public static class DefaultJsonSerializerOptions +{ + public static readonly JsonSerializerOptions Default = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public static readonly JsonSerializerOptions CamelCasePrettyPrint = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; +} \ No newline at end of file diff --git a/MycroForge.Core/Extensions/ObjectStreamExtensions.cs b/MycroForge.Core/Extensions/ObjectStreamExtensions.cs index 0a231e8..5d8a38f 100644 --- a/MycroForge.Core/Extensions/ObjectStreamExtensions.cs +++ b/MycroForge.Core/Extensions/ObjectStreamExtensions.cs @@ -4,15 +4,12 @@ namespace MycroForge.Core.Extensions; public static class ObjectStreamExtensions { - public static async Task SerializeAsync( - this object @object, - JsonSerializerOptions? jsonSerializerOptions = null - ) + public static async Task SerializeAsync(this object @object, JsonSerializerOptions? options = null) { using var stream = new MemoryStream(); using var reader = new StreamReader(stream); - var options = jsonSerializerOptions ?? Serialization.DefaultJsonSerializerOptions.Default; - + options ??= DefaultJsonSerializerOptions.Default; + await JsonSerializer.SerializeAsync(stream, @object, options); stream.Position = 0; return await reader.ReadToEndAsync(); diff --git a/MycroForge.Core/ProjectContext.cs b/MycroForge.Core/ProjectContext.cs index ae93142..2b9bbf8 100644 --- a/MycroForge.Core/ProjectContext.cs +++ b/MycroForge.Core/ProjectContext.cs @@ -21,8 +21,8 @@ public class ProjectContext } var config = await JsonSerializer.DeserializeAsync( - File.OpenRead(ConfigPath), - Serialization.DefaultJsonSerializerOptions.CamelCasePrettyPrint + File.OpenRead(ConfigPath), + DefaultJsonSerializerOptions.CamelCasePrettyPrint ); if (config is null) @@ -116,7 +116,7 @@ public class ProjectContext public async Task SaveConfig(ProjectConfig config) { - var json = await config.SerializeAsync(Serialization.DefaultJsonSerializerOptions.CamelCasePrettyPrint); + var json = await config.SerializeAsync(DefaultJsonSerializerOptions.CamelCasePrettyPrint); await File.WriteAllTextAsync(ConfigPath, json); } } \ No newline at end of file diff --git a/MycroForge.Core/Serialization.cs b/MycroForge.Core/Serialization.cs deleted file mode 100644 index 86faf78..0000000 --- a/MycroForge.Core/Serialization.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.Json; - -namespace MycroForge.Core; - -public static class Serialization -{ - public static class DefaultJsonSerializerOptions - { - public static readonly JsonSerializerOptions Default = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - public static readonly JsonSerializerOptions CamelCasePrettyPrint = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; - } -} \ No newline at end of file diff --git a/MycroForge.sln b/MycroForge.sln index 6fb7fac..74b7394 100644 --- a/MycroForge.sln +++ b/MycroForge.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate.Package", "MycroForge.PluginTemplate.Package\MycroForge.PluginTemplate.Package.csproj", "{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.CLI.Tests", "MycroForge.CLI.Tests\MycroForge.CLI.Tests.csproj", "{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +32,9 @@ Global {1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.Build.0 = Release|Any CPU + {71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal