Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions GodotEnv.Tests/src/features/godot/commands/GodotLaunchCommandTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace Chickensoft.GodotEnv.Tests.features.godot.commands;

using System;
using System.IO;
using System.Threading.Tasks;
using Chickensoft.GodotEnv.Common.Clients;
using Chickensoft.GodotEnv.Features.Godot.Commands;
using Chickensoft.GodotEnv.Features.Godot.Domain;
using Chickensoft.GodotEnv.Features.Godot.Models;
using CliFx.Infrastructure;
using Common.Models;
using Common.Utilities;
using global::GodotEnv.Common.Utilities;
using Moq;
using Shouldly;
using Xunit;

public sealed class GodotLaunchCommandTest : IDisposable {
private readonly MockSystemInfo _systemInfo;
private readonly Mock<IExecutionContext> _context;
private readonly Mock<IGodotContext> _godotContext;
private readonly Mock<IGodotEnvironment> _environment;
private readonly Mock<IGodotRepository> _godotRepo;
private readonly Mock<IProcessRunner> _processRunner;
private readonly FakeInMemoryConsole _console;
private readonly Mock<IFileClient> _fileClient;
private readonly Log _log;

public GodotLaunchCommandTest() {
_systemInfo = new MockSystemInfo(OSType.Linux, CPUArch.X64);
_context = new Mock<IExecutionContext>();
_godotContext = new Mock<IGodotContext>();
_environment = new Mock<IGodotEnvironment>();
_godotRepo = new Mock<IGodotRepository>();
_processRunner = new Mock<IProcessRunner>();
_console = new FakeInMemoryConsole();
_fileClient = new Mock<IFileClient>();


_environment.Setup(env => env.SystemInfo).Returns(_systemInfo);
_godotContext.SetupGet(c => c.GodotRepo).Returns(_godotRepo.Object);
_godotContext.Setup(c => c.Platform).Returns(_environment.Object);
_context.SetupGet(context => context.Godot).Returns(_godotContext.Object);
_log = new Log(_systemInfo, _console);
_context.Setup(context => context.CreateLog(_console)).Returns(_log);

_godotRepo.SetupGet(r => r.ProcessRunner).Returns(_processRunner.Object);
}

public void Dispose() {
_console.Dispose();
Environment.SetEnvironmentVariable("GODOT", null);
}

[Fact]
public async Task Launches_Godot_When_Env_Variable_Is_Valid() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test names in Chickensoft projects are usually just PascalCase, with no underscores.

var godotPath = Path.Combine(Path.GetTempPath(), "godot-test-launcher");

// Create a fake Godot binary
File.WriteAllText(godotPath, string.Empty);
Environment.SetEnvironmentVariable("GODOT", godotPath);

var launchCommand = new GodotLaunchCommand(_context.Object);

await launchCommand.ExecuteAsync(_console);

_processRunner.Verify(p =>
p.RunDetached(godotPath, Array.Empty<string>()), Times.Once
);

_log.ToString().ShouldContain($"Launching Godot from {godotPath}");

// Clean up
File.Delete(godotPath);
}

[Fact]
public async Task Fails_When_GODOT_Variable_Is_Unset() {
Environment.SetEnvironmentVariable("GODOT", null);

var launchCommand = new GodotLaunchCommand(_context.Object);
await launchCommand.ExecuteAsync(_console);

_processRunner.Verify(p =>
p.RunDetached(It.IsAny<string>(), It.IsAny<string[]>()), Times.Never
);

_log.ToString().ShouldContain("The GODOT environment variable is not set");
}

[Fact]
public async Task Fails_When_GODOT_Target_Does_Not_Exist() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides the underscores, I'd suggest using EnvVariable or something in place of GODOT -- all-caps isn't preferred, but PascalCase could lead to confusion between Godot itself and the environment variable.

var godotPath = "/nonexistent/godot";
Environment.SetEnvironmentVariable("GODOT", godotPath);

var launchCommand = new GodotLaunchCommand(_context.Object);
await launchCommand.ExecuteAsync(_console);

_processRunner.Verify(p =>
p.RunDetached(It.IsAny<string>(), It.IsAny<string[]>()), Times.Never
);

_log.ToString().ShouldContain($"The GODOT environment variable points to a missing file: {godotPath}");
}
}

31 changes: 30 additions & 1 deletion GodotEnv/src/common/utilities/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ public interface IProcessRunner {
/// </param>
/// <param name="args">Process arguments.</param>
/// <returns>Process result task.</returns>
Task<ProcessResult> Run(string workingDir, string exe, string[] args);
Task<ProcessResult> Run(string workingDir, string exe, string[] args);

/// <summary>
/// Starts a detached process. (e.g. launching a GUI app) without waiting for it.
/// </summary>
/// <param name="args">Process arguments.
/// </param>
/// <param name="exe">Process to run (must be in the system shell's path).
/// </param>
Task RunDetached(string exe, string[] args);

/// <summary>
/// Runs an external process with callbacks for stdout and stderr.
Expand Down Expand Up @@ -85,6 +94,26 @@ public async Task<ProcessResult> Run(
StandardOutput: stdOutBuffer.ToString(),
StandardError: stdErrBuffer.ToString()
);
}

// This method is used to run a detached process.
public Task RunDetached(string exe, string[] args) {
var startInfo = new ProcessStartInfo {
FileName = exe,
Arguments = string.Join(" ", args),
UseShellExecute = true,
CreateNoWindow = true,
WorkingDirectory = Environment.CurrentDirectory
};

try {
Process.Start(startInfo);
}
catch (Exception ex) {
Console.Error.WriteLine($"Failed to launch detached process: {ex.Message}");
}

return Task.CompletedTask;
}

public async Task<ProcessResult> RunElevatedOnWindows(
Expand Down
47 changes: 47 additions & 0 deletions GodotEnv/src/features/godot/commands/GodotLaunchCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace Chickensoft.GodotEnv.Features.Godot.Commands;

using System;
using System.IO;
using System.Threading.Tasks;
using Chickensoft.GodotEnv.Common.Clients;
using Chickensoft.GodotEnv.Common.Models;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using global::GodotEnv.Common.Utilities;

[Command("godot launch", Description = "Launches the currently active Godot version.")]
public class GodotLaunchCommand : ICommand, ICliCommand {
public IExecutionContext ExecutionContext { get; set; } = default!;

public GodotLaunchCommand(IExecutionContext context) {
ExecutionContext = context;
}

private ISystemInfo SystemInfo => ExecutionContext.Godot.Platform.SystemInfo;


public async ValueTask ExecuteAsync(IConsole console) {
var log = ExecutionContext.CreateLog(console);

string? godotPath = Environment.GetEnvironmentVariable("GODOT");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this, I would suggest we wait til #101 is merged and then do:

Suggested change
string? godotPath = Environment.GetEnvironmentVariable("GODOT");
var godotRepo = ExecutionContext.Godot.GodotRepo;
var godotPath = godotRepo.GodotSymlinkTarget;

And then update tests to match. (The reason to wait on #101 is that GodotSymlinkTarget is broken on Windows until that's in.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this change, why should we reference the SymLink and not the environment variable directly? It was my understanding after speaking with @jolexxa that we should reference off of the EnvVariable?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay. Wait for @jolexxa to weigh in, but in terms of my rationale:

  1. The environment variable should be pointing to the symlink. (If it isn't, the user's system is configured in a way that's incompatible with GodotEnv.) Given that, we may as well use the symlink directly, instead of referring to the environment variable that's referring to the symlink.
  2. If the user is launching from CLI with GodotEnv, they should expect GodotEnv to behave in a way that's consistent with the rest of its behavior. Unless I've missed something, there are relatively few places in the codebase where GodotEnv actually uses the GODOT environment variable; it sets it and will print it on the user's request, but that's about it. On the other hand, the GodotRepository uses the symlink location and target for managing the installations, changing the environment variable, and updating the start menu/desktop shortcuts. So all in all, I think it's more consistent with the rest of GodotEnv's behavior to use the symlink here too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheDevRatt what Mark said, essentially. The env var is for the user to launch Godot via the symlink and for use in vscode launch configs or any other scripts that need to point to Godot. Since we are managing the symlink, we can just access it directly.


if (string.IsNullOrWhiteSpace(godotPath)) {
log.Err("❌ The GODOT environment variable is not set.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (not 100% sure) that @jolexxa has "removing emoji from the output" on her roadmap for GodotEnv, so we may not want to add new ones. Maybe she can confirm if I'm remembering that correctly, though.

log.Print("To set it, use:\n godotenv godot use <version>\n");
return;
}

if (SystemInfo.OS == OSType.Windows && !godotPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) {
godotPath += ".exe";
}
Comment on lines +35 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here again I'd suggest waiting for #101 - this will also be handled by GodotRepository.GodotSymlinkTarget once that's merged, so we won't need this block at all.


if (!File.Exists(godotPath)) {
log.Err($"❌ The GODOT environment variable points to a missing file: {godotPath}");
return;
}

log.Print($"🚀 Launching Godot from {godotPath}...");
await ExecutionContext.Godot.GodotRepo.ProcessRunner.RunDetached(godotPath, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await ExecutionContext.Godot.GodotRepo.ProcessRunner.RunDetached(godotPath, []);
var computer = ExecutionContext.Godot.Platform.Computer;
var shell = computer.CreateShell(ExecutionContext.WorkingDir);
shell.RunDetached(godotPath, []);

This suggestion will require implementing Shell.RunDetached(string executable, string[] args) and adding it to IShell.

}
}