Basic project creation (#3)

This PR contains the work done to create a new *Hummingbird* project with very basic configuration from the _colibri_ executable, just like the project you could create with the [Hummingbird template](https://github.com/hummingbird-project/template) project in Github.

Reviewed-on: #3
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #3.
This commit is contained in:
2025-01-28 00:07:24 +00:00
committed by Javier Cicchelli
parent b8c354e614
commit 9be8fa4a31
52 changed files with 1936 additions and 475 deletions
@@ -0,0 +1,125 @@
import Testing
@testable import ColibriLibrary
struct FileTests {
// MARK: Properties tests
@Test(arguments: zip(File.allCases, Expectation.fileNames))
func fileName(for file: File, expects fileName: String) async throws {
// GIVEN
// WHEN
let result = file.fileName
// THEN
#expect(result == fileName)
}
@Test(arguments: zip(File.allCases, Expectation.filePaths))
func filePath(for file: File, expects filePath: String) async throws {
// GIVEN
// WHEN
let result = file.filePath
// THEN
#expect(result == filePath)
}
@Test(arguments: zip(File.allCases, Expectation.folders))
func folder(for file: File, expects folder: Folder) async throws {
// GIVEN
// WHEN
let result = file.folder
// THEN
#expect(result == folder)
}
@Test(arguments: zip(File.allCases, Expectation.resourcePaths))
func resourcePath(for file: File, expects resourcePath: String) async throws {
// GIVEN
// WHEN
let result = file.resourcePath
// THEN
#expect(result == resourcePath)
}
}
// MARK: - Expectations
private extension FileTests {
enum Expectation {
static let fileNames: [String] = [
"App.swift",
"AppArguments.swift",
"AppBuilder.swift",
"AppOptions.swift",
"AppTests.swift",
"Dockerfile",
".dockerignore",
"Environment+Properties.swift",
".gitignore",
"LICENSE",
"LoggerLevel+Conformances.swift",
"Package.swift",
"README.md",
"TestArguments.swift"
]
static let filePaths: [String] = [
"App/Sources/App.swift",
"Library/Sources/Public/AppArguments.swift",
"Library/Sources/Public/AppBuilder.swift",
"App/Sources/AppOptions.swift",
"Test/Sources/Cases/Public/AppTests.swift",
"Dockerfile",
".dockerignore",
"Library/Sources/Internal/Environment+Properties.swift",
".gitignore",
"LICENSE",
"Library/Sources/Internal/LoggerLevel+Conformances.swift",
"Package.swift",
"README.md",
"Test/Sources/Helpers/TestArguments.swift"
]
static let folders: [Folder] = [
.app,
.libraryPublic,
.libraryPublic,
.app,
.testCasesPublic,
.root,
.root,
.libraryInternal,
.root,
.root,
.libraryInternal,
.root,
.root,
.testHelpers
]
static let resourcePaths: [String] = [
"Resources/Files/Sources/App",
"Resources/Files/Sources/Library",
"Resources/Files/Sources/Library",
"Resources/Files/Sources/App",
"Resources/Files/Sources/Test",
"Resources/Files/Sources",
"Resources/Files/Sources",
"Resources/Files/Sources/Library",
"Resources/Files/Sources",
"Resources/Files/Sources",
"Resources/Files/Sources/Library",
"Resources/Files/Sources",
"Resources/Files/Sources",
"Resources/Files/Sources/Test"
]
}
}
@@ -0,0 +1,35 @@
import Testing
@testable import ColibriLibrary
struct FolderTests {
// MARK: Properties tests
@Test(arguments: zip(Folder.allCasesWithRoot, Expectation.paths))
func paths(for folder: Folder, expects path: String) async throws {
// GIVEN
// WHEN
let result = folder.path
// THEN
#expect(result == path)
}
}
// MARK: - Expectations
private extension FolderTests {
enum Expectation {
static let paths: [String] = [
"",
"App/Sources/",
"Library/Sources/Public/",
"Library/Sources/Internal/",
"Test/Sources/Cases/Public/",
"Test/Sources/Cases/Internal/",
"Test/Sources/Helpers/"
]
}
}
@@ -0,0 +1,76 @@
import Foundation
import Testing
@testable import ColibriLibrary
struct URL_ExtensionsTests {
// MARK: Initialisers tests
@Test(arguments: zip([String.someFilePath, .dotPath, .tildePath],
[URL.someFile, .dotFile, .tildeFile]))
func initAt(
with filePath: String,
expects url: URL
) async throws {
// GIVEN
// WHEN
let result = URL(at: filePath)
// THEN
#expect(result == url)
#expect(result.isFileURL == true)
}
// MARK: Computed tests
@Test(arguments: zip([URL.someFile, .dotFile, .tildeFile, .someURL],
[String.someFilePath, .dotPath, .tildePath, .empty]))
func pathString(
with url: URL,
expects path: String
) async throws {
// GIVEN
// WHEN
let result = url.pathString
// THEN
#expect(result == path)
}
// MARK: Functions tests
@Test(arguments: zip([URL.dotFile, .tildeFile, .someFile],
[".\(String.someFilePath)", "~\(String.someFilePath)", "\(String.someFilePath)\(String.someFilePath)"]))
func appendingPath(
with url: URL,
expects path: String
) async throws {
// GIVEN
// WHEN
let result = url.appendingPath(.someFilePath)
// THEN
#expect(result.pathString == path)
#expect(result.isFileURL == true)
}
}
// MARK: - String+Constants
private extension String {
static let dotPath = "."
static let empty = ""
static let tildePath = "~"
static let someFilePath = "/some/file/path"
}
// MARK: - URL+Constants
private extension URL {
static let dotFile = URL(at: .dotPath)
static let someFile = URL(at: .someFilePath)
static let someURL = URL(string: "https://some.url.path")!
static let tildeFile = URL(at: .tildePath)
}
@@ -0,0 +1,68 @@
import Foundation
import Testing
@testable import ColibriLibrary
struct RunProcessTaskTests {
// MARK: Properties
private var process: Process
// MARK: Initialisers
init() {
self.process = Process()
}
// MARK: Functions tests
@Test(arguments: [Argument.empty, Argument.listAllInFolder])
func run(with arguments: [String]) async throws {
// GIVEN
var task = RunProcessTask(process: process)
// WHEN
let output = try await task(path: .ls, arguments: arguments)
// THEN
#expect(output.isEmpty == false)
}
@Test(arguments: zip([Argument.help, Argument.listAllInPWD], Throw.outputs))
func runThrows(with arguments: [String], throws error: RunProcessError) async throws {
// GIVEN
var task = RunProcessTask(process: process)
// WHEN
// THEN
await #expect(throws: error) {
try await task(path: .ls, arguments: arguments)
}
}
}
// MARK: - String+Constants
private extension String {
static let ls = "/bin/ls"
}
// MARK: - Parameters
private extension RunProcessTaskTests {
enum Argument {
static let empty: [String] = []
static let help: [String] = ["--help"]
static let listAllInFolder: [String] = ["-la", "."]
static let listAllInPWD: [String] = ["-la", "~"]
}
enum Throw {
static let outputs: [RunProcessError] = [
.output("ls: unrecognized option `--help\'\nusage: ls [-@ABCFGHILOPRSTUWXabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n"),
.output("ls: ~: No such file or directory\n")
]
}
}
@@ -0,0 +1,181 @@
import ColibriLibrary
import Foundation
import Testing
struct FileServiceTests {
// MARK: Properties
private let spy = FileServiceSpy()
// MARK: Properties tests
@Test func currentFolder() async {
// GIVEN
let url: URL = .someCurrentFolder
let service = FileServiceMock(currentFolder: url)
// WHEN
let folder = await service.currentFolder
// THEN
#expect(folder == url)
#expect(folder.isFileURL == true)
}
// MARK: Functions tests
@Test(arguments: zip([URL.someExistingFile, .someExistingFolder],
[URL.someNewFile, .someNewFolder]))
func copyFile(from source: URL, to destination: URL) async throws {
// GIVEN
let service = service(action: .copyFile(source, destination))
// WHEN
try await service.copyFile(from: source, to: destination)
// THENn
#expect(spy.actions.count == 1)
let action = try #require(spy.actions.last)
#expect(action == .fileCopied(source, destination))
}
@Test(arguments: [FileServiceError.itemAlreadyExists, .itemEmptyData, .itemNotCopied])
func copyItem(throws error: FileServiceError) async throws {
// GIVEN
let service = service(action: .error(error))
// WHEN
// THEN
await #expect(throws: error) {
try await service.copyFile(from: .someExistingFile, to: .someNewFile)
}
#expect(spy.actions.isEmpty == true)
}
@Test(arguments: [URL.someNewFolder, .someNewFile])
func createFolder(with location: URL) async throws {
// GIVEN
let service = service(action: .createFolder(location))
// WHEN
try await service.createFolder(at: location)
// THEN
#expect(spy.actions.count == 1)
let action = try #require(spy.actions.last)
#expect(action == .folderCreated(location))
}
@Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someRandomURL],
[FileServiceError.itemAlreadyExists, .itemAlreadyExists, .itemNotFileURL]))
func createFolder(
with location: URL,
throws error: FileServiceError
) async throws {
// GIVEN
let service = service(action: .error(error))
// WHEN
// THEN
await #expect(throws: error) {
try await service.createFolder(at: location)
}
#expect(spy.actions.isEmpty == true)
}
@Test(arguments: [URL.someNewFolder, .someNewFile])
func deleteItem(with location: URL) async throws {
// GIVEN
let service = service(action: .deleteItem(location))
// WHEN
try await service.deleteItem(at: location)
// THEN
#expect(spy.actions.count == 1)
let action = try #require(spy.actions.last)
#expect(action == .itemDeleted(location))
}
@Test(arguments: zip([URL.someNewFolder, .someNewFile, .someRandomURL],
[FileServiceError.itemNotExists, .itemNotExists, .itemNotFileURL]))
func deleteItem(
with location: URL,
throws error: FileServiceError
) async throws {
// GIVEN
let service = service(action: .error(error))
// WHEN
// THEN
await #expect(throws: error) {
try await service.deleteItem(at: location)
}
#expect(spy.actions.isEmpty == true)
}
@Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNewFolder, .someNewFile],
[true, true, false, false]))
func isItemExists(
with location: URL,
expects outcome: Bool
) async throws {
// GIVEN
let service = service(action: .isItemExists(location, outcome))
// WHEN
let result = try await service.isItemExists(at: location)
// THEN
#expect(result == outcome)
let action = try #require(spy.actions.last)
#expect(action == .itemExists(location))
}
@Test(arguments: zip([URL.someRandomURL], [FileServiceError.itemNotFileURL]))
func isItemExists(
with location: URL,
throws error: FileServiceError
) async throws {
// GIVEN
let service = service(action: .error(error))
// WHEN
// THEN
await #expect(throws: error) {
try await service.isItemExists(at: location)
}
#expect(spy.actions.isEmpty == true)
}
}
// MARK: - Helpers
private extension FileServiceTests {
// MARK: Functions
func service(action: FileServiceMock.Action) -> FileServiceMock {
.init(
currentFolder: .someCurrentFolder,
action: action,
spy: spy
)
}
}
@@ -0,0 +1,80 @@
import Foundation
import Testing
@testable import ColibriLibrary
struct CopyFilesTaskTests {
// MARK: Properties
private let resourceFolder = URL.someExistingFolder
private let rootFolder = URL.someNewFolder
private let spy = FileServiceSpy()
// MARK: Functions tests
@Test func copyFiles() async throws {
// GIVEN
let files = files(of: File.allCases)
let actions = files.map { FileServiceMock.Action.copyFile($0.source, $0.destination) }
let copyFiles = task(actions: actions)
// WHEN
try await copyFiles(to: rootFolder)
// THEN
#expect(spy.actions.count == actions.count)
files.enumerated().forEach { index, file in
#expect(spy.actions[index] == .fileCopied(file.source, file.destination))
}
}
@Test(arguments: [FileServiceError.itemAlreadyExists, .itemEmptyData, .itemNotCopied])
func copyFiles(throws error: FileServiceError) async throws {
// GIVEN
let files = files(of: Array(File.allCases[0...2]))
let actions = files.map { FileServiceMock.Action.copyFile($0.source, $0.destination) }
let copyFiles = task(actions: actions + [.error(error)])
// WHEN
// THEN
await #expect(throws: error) {
try await copyFiles(to: rootFolder)
}
#expect(spy.actions.count == actions.count)
files.enumerated().forEach { index, file in
#expect(spy.actions[index] == .fileCopied(file.source, file.destination))
}
}
}
// MARK: - Helpers
private extension CopyFilesTaskTests {
// MARK: Type aliases
typealias FileURL = (source: URL, destination: URL)
// MARK: Functions
func files(of resourceFiles: [File]) -> [FileURL] {
resourceFiles.map { (resourceFolder.appendingPath($0.rawValue), rootFolder.appendingPath($0.fileName)) }
}
func task(actions: [FileServiceMock.Action]) -> CopyFilesTask {
.init(fileService: FileServiceMock(
currentFolder: .someCurrentFolder,
actions: actions,
spy: spy
))
}
}
@@ -0,0 +1,49 @@
import Foundation
import Testing
@testable import ColibriLibrary
struct CreateFoldersTaskTests {
// MARK: Properties
private let spy = FileServiceSpy()
// MARK: Functions tests
@Test(arguments: [URL.someCurrentFolder, .someDotFolder, .someTildeFolder])
func createFolders(with rootFolder: URL) async throws {
// GIVEN
let folders = Folder.allCases.map { rootFolder.appendingPath($0.path) }
let actions = folders.map { FileServiceMock.Action.createFolder($0) }
let createFolders = task(actions: actions)
// WHEN
try await createFolders(at: rootFolder)
// THEN
#expect(spy.actions.count == actions.count)
for index in actions.indices {
#expect(spy.actions[index] == .folderCreated(folders[index]))
}
}
}
// MARK: - Helpers
private extension CreateFoldersTaskTests {
// MARK: Functions
func task(actions: [FileServiceMock.Action]) -> CreateFoldersTask {
.init(fileService: FileServiceMock(
currentFolder: .someCurrentFolder,
actions: actions,
spy: spy
))
}
}
@@ -0,0 +1,88 @@
import Foundation
import Testing
@testable import ColibriLibrary
struct CreateRootFolderTaskTests {
// MARK: Functions tests
@Test(arguments: [String.someProjectName], [URL.someCurrentProjectFolder, .someNewProjectFolder, .someDotProjectFolder, .someTildeProjectFolder])
func task(
name: String,
expects folder: URL
) async throws {
// GIVEN
let location: URL? = switch folder {
case .someNewProjectFolder: .someNewFolder
case .someDotProjectFolder: .someDotFolder
case .someTildeProjectFolder: .someTildeFolder
default: nil
}
let task = CreateRootFolderTask(fileService: FileServiceMock(
currentFolder: .someCurrentFolder,
action: .createFolder(folder)
))
// WHEN
let result = try await task(name: name,
at: location)
// THEN
#expect(result == folder)
#expect(result.isFileURL == true)
}
@Test(arguments: [String.someProjectName], [FileServiceError.itemAlreadyExists])
func task(
name: String,
throws error: FileServiceError
) async throws {
// GIVEN
let task = CreateRootFolderTask(fileService: FileServiceMock(
currentFolder: .someCurrentFolder,
action: .error(error)
))
// WHEN
// THEN
await #expect(throws: error) {
try await task(name: name)
}
}
@Test(arguments: [String.someEmptyName], [CreateRootFolderError.nameIsEmpty])
func task(
name: String,
throws error: CreateRootFolderError
) async throws {
// GIVEN
let task = CreateRootFolderTask(fileService: FileServiceMock(
currentFolder: .someCurrentFolder
))
// WHEN
// THEN
await #expect(throws: error) {
try await task(name: name)
}
}
}
// MARK: - String+Constants
private extension String {
static let someEmptyName = ""
static let someProjectName = "SomeProjectName"
}
// MARK: - URL+Constants
private extension URL {
static let someCurrentProjectFolder = URL.someCurrentFolder.appendingPath(.someProjectName)
static let someDotProjectFolder = URL.someDotFolder.appendingPath(.someProjectName)
static let someNewProjectFolder = URL.someNewFolder.appendingPath(.someProjectName)
static let someTildeProjectFolder = URL.someTildeFolder.appendingPath(.someProjectName)
}
@@ -0,0 +1,18 @@
import Foundation
@testable import ColibriLibrary
extension URL {
// MARK: Constants
static let someCurrentFolder = URL(at: "/some/current/folder")
static let someDotFolder = URL(at: ".")
static let someExistingFolder = URL(at: "/some/existing/folder")
static let someExistingFile = URL(at: "/some/existing/file")
static let someNewFolder = URL(at: "/some/new/folder")
static let someNewFile = URL(at: "/some/new/file")
static let someRandomURL = URL(string: "http://some.random.url")!
static let someTildeFolder = URL(at: "~")
}
@@ -0,0 +1,135 @@
import ColibriLibrary
import Foundation
final class FileServiceMock {
// MARK: Properties
private let folder: URL
private var actions: [Action] = []
private weak var spy: FileServiceSpy?
// MARK: Initialisers
init(
currentFolder: URL,
action: Action? = nil,
spy: FileServiceSpy? = nil
) {
self.actions = if let action {
[action]
} else {
[]
}
self.folder = currentFolder
self.spy = spy
}
init(
currentFolder: URL,
actions: [Action],
spy: FileServiceSpy? = nil
) {
self.actions = actions
self.folder = currentFolder
self.spy = spy
}
}
// MARK: - FileServicing
extension FileServiceMock: FileServicing {
// MARK: Computed
var currentFolder: URL {
get async { folder }
}
// MARK: Functions
func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) {
guard let nextAction else { return }
switch nextAction {
case .error(let error):
throw error
case let .copyFile(source, destination):
try await spy?.copyFile(from: source, to: destination)
default:
break
}
}
func createFolder(at location: URL) async throws (FileServiceError) {
guard let nextAction else { return }
switch nextAction {
case .error(let error):
throw error
case let .createFolder(location):
try await spy?.createFolder(at: location)
default:
break
}
}
func deleteItem(at location: URL) async throws (FileServiceError) {
guard let nextAction else { return }
switch nextAction {
case .error(let error):
throw error
case let .deleteItem(location):
try await spy?.deleteItem(at: location)
default:
break
}
}
func isItemExists(at location: URL) async throws (FileServiceError) -> Bool {
guard let nextAction else { return false }
switch nextAction {
case .error(let error):
throw error
case let .isItemExists(location, exists):
try await spy?.isItemExists(at: location)
return exists
default:
return false
}
}
}
// MARK: - Helpers
private extension FileServiceMock {
// MARK: Computed
var nextAction: Action? {
guard !actions.isEmpty else {
return nil
}
return actions.removeFirst()
}
}
// MARK: - Actions
extension FileServiceMock {
enum Action {
case copyFile(URL, URL)
case createFolder(URL)
case deleteItem(URL)
case error(FileServiceError)
case isItemExists(URL, Bool)
}
}
@@ -0,0 +1,51 @@
import Foundation
@testable import ColibriLibrary
final class FileServiceSpy {
// MARK: Properties
private(set) var actions: [Action] = []
}
// MARK: - FileServicing
extension FileServiceSpy: FileServicing {
var currentFolder: URL {
get async { .someCurrentFolder }
}
func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) {
actions.append(.fileCopied(source, destination))
}
func createFolder(at location: URL) async throws (FileServiceError) {
actions.append(.folderCreated(location))
}
func deleteItem(at location: URL) async throws (FileServiceError) {
actions.append(.itemDeleted(location))
}
@discardableResult
func isItemExists(at location: URL) async throws (FileServiceError) -> Bool {
actions.append(.itemExists(location))
return .random()
}
}
// MARK: - Action
extension FileServiceSpy {
enum Action: Equatable {
case fileCopied(_ source: URL, _ destination: URL)
case folderCreated(_ location: URL)
case itemDeleted(_ location: URL)
case itemExists(_ location: URL)
}
}