Added CLI test project and tests for 'm4g init' command

This commit is contained in:
mdnapo 2024-10-05 13:37:14 +02:00
parent 4f322e56c7
commit f676f236b1
12 changed files with 338 additions and 30 deletions

19
.dockerignore Normal file
View File

@ -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

30
Dockerfile Normal file
View File

@ -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"]

View File

@ -0,0 +1,11 @@
using DotNet.Testcontainers.Containers;
namespace MycroForge.CLI.Tests.Extensions;
internal static class ContainerInterfaceExtensions
{
public static Task<byte[]> ReadFileFromRootAsync(this IContainer container, string file)
{
return container.ReadFileAsync($"/test/todo/{file}");
}
}

View File

@ -0,0 +1 @@
global using NUnit.Framework;

View File

@ -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<ProjectConfig>(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"));
});
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Testcontainers" Version="3.10.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MycroForge.Core\MycroForge.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
#!/usr/bin/bash
#!/bin/bash
dotnet pack -v d

View File

@ -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
};
}

View File

@ -4,15 +4,12 @@ namespace MycroForge.Core.Extensions;
public static class ObjectStreamExtensions
{
public static async Task<string> SerializeAsync(
this object @object,
JsonSerializerOptions? jsonSerializerOptions = null
)
public static async Task<string> 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();

View File

@ -21,8 +21,8 @@ public class ProjectContext
}
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(
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);
}
}

View File

@ -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
};
}
}

View File

@ -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