Skip to content
Merged
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
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ jobs:
path: build/coverage/

package:
needs: test
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
needs: [build-go, test]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -87,6 +86,9 @@ jobs:
name: matlab-http-bridge-binaries
path: toolbox/bin/

- name: Restore Linux execute permission
run: chmod +x toolbox/bin/glnxa64/matlab-http-bridge

- name: Set up MATLAB
uses: matlab-actions/setup-matlab@v2

Expand All @@ -103,7 +105,7 @@ jobs:
path: matlab-http-server.mltbx

- name: Create Release
if: startsWith(github.ref, 'refs/tags/v')
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: matlab-http-server.mltbx
Expand Down
12 changes: 6 additions & 6 deletions buildfile.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
% Default task is 'test'
plan.DefaultTasks = "test";

% Package depends on test
plan("package").Dependencies = "test";
end

function testAction(context)
Expand All @@ -41,14 +39,16 @@ function testAction(context)
% Cobertura and HTML
xmlFile = fullfile(covFolder, "coverage.xml");

% Target only .m files in toolbox/ and its subfolders, excluding doc/ and examples/
% Target only runtime .m files in toolbox/ and its subfolders,
% excluding documentation and examples.
allFiles = dir(fullfile("toolbox", "**", "*.m"));
allFiles = allFiles(~[allFiles.isdir]);
sourceFiles = fullfile({allFiles.folder}, {allFiles.name});

% Filter out non-m files and examples/
% Exclude documentation and examples from runtime coverage checks.
isExample = contains(sourceFiles, fullfile("toolbox", "examples"));
sourceFiles = sourceFiles(endsWith(sourceFiles, ".m") & ~isExample);
isDoc = contains(sourceFiles, fullfile("toolbox", "doc"));
sourceFiles = sourceFiles(endsWith(sourceFiles, ".m") & ~isExample & ~isDoc);

% Use multiple formats in one plugin call
formats = [CoverageReport(covFolder), CoberturaFormat(xmlFile)];
Expand Down Expand Up @@ -95,4 +95,4 @@ function packageAction(context)
fprintf('Packaging %s into %s...\n', prjFile, outFile);
matlab.addons.toolbox.packageToolbox(prjFile, outFile);
end
end
end
86 changes: 86 additions & 0 deletions tests/TestGoSidecarTransport.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,68 @@ function testBinaryNotFoundErrors(testCase)
testCase.verifyTrue(true);
end

function testBinaryRelativePathForWindows(testCase)
relativePath = mhs.internal.GoSidecarTransport ...
.binaryRelativePathForPlatform(true, false, "win64");

testCase.verifyEqual(string(relativePath), ...
string(fullfile("bin", "win64", "matlab-http-bridge.exe")));
end

function testBinaryRelativePathForMacArm(testCase)
relativePath = mhs.internal.GoSidecarTransport ...
.binaryRelativePathForPlatform(false, true, "maca64");

testCase.verifyEqual(string(relativePath), ...
string(fullfile("bin", "maca64", "matlab-http-bridge")));
end

function testBinaryRelativePathForMacIntel(testCase)
relativePath = mhs.internal.GoSidecarTransport ...
.binaryRelativePathForPlatform(false, true, "maci64");

testCase.verifyEqual(string(relativePath), ...
string(fullfile("bin", "maci64", "matlab-http-bridge")));
end

function testBinaryRelativePathForLinux(testCase)
relativePath = mhs.internal.GoSidecarTransport ...
.binaryRelativePathForPlatform(false, false, "glnxa64");

testCase.verifyEqual(string(relativePath), ...
string(fullfile("bin", "glnxa64", "matlab-http-bridge")));
end

function testFindBinaryForPlatformErrorsWhenMissing(testCase)
missingRoot = fullfile(tempdir, "mhs-missing-binary-" + string(java.util.UUID.randomUUID));
mkdir(missingRoot);
cleanup = onCleanup(@() rmdir(missingRoot, "s"));

testCase.verifyError(@() ...
mhs.internal.GoSidecarTransport.findBinaryForPlatform( ...
string(missingRoot), false, false, "glnxa64"), ...
"MatlabHttpServer:binaryNotFound");
clear cleanup;
end

function testEnsureBinaryExecutableNoOpsForExistingExecutable(testCase)
testCase.Transport = mhs.internal.GoSidecarTransport(testCase.Port);
testCase.verifyWarningFree(@() ...
mhs.internal.GoSidecarTransport.ensureBinaryExecutable( ...
testCase.Transport.BinaryPath));
end

function testFindBinaryForPlatformReturnsExistingBinary(testCase)
testCase.Transport = mhs.internal.GoSidecarTransport(testCase.Port);
toolboxRoot = string(fileparts(fileparts(fileparts( ...
testCase.Transport.BinaryPath))));

resolved = mhs.internal.GoSidecarTransport.findBinaryForPlatform( ...
toolboxRoot, ispc, ismac, string(computer('arch')));

testCase.verifyEqual(resolved, testCase.Transport.BinaryPath);
end

function testStartStop(testCase)
testCase.Transport = mhs.internal.GoSidecarTransport(testCase.Port);
testCase.Transport.start();
Expand Down Expand Up @@ -113,6 +175,30 @@ function testParseResponseBytes(testCase)
testCase.verifyEqual(char(body), '{"status":"ok"}');
end

function testWriteResponseSerializesHeadersForJson(testCase)
testCase.Transport = mhs.internal.GoSidecarTransport(testCase.Port);
writer = java.io.StringWriter();
testCase.Transport.Writer = java.io.PrintWriter(writer, true);

CRLF = char([13 10]);
responseBytes = uint8([ ...
'HTTP/1.1 201 Created' CRLF ...
'Content-Type: application/json' CRLF ...
'X-Test: value' CRLF ...
CRLF ...
'{"ok":true}']);

socket = struct("id", "req-123", "transport", testCase.Transport);
testCase.Transport.writeResponse(socket, responseBytes);

payload = jsondecode(char(writer.toString()));
testCase.verifyEqual(string(payload.id), "req-123");
testCase.verifyEqual(payload.status, 201);
testCase.verifyEqual(string(payload.headers.Content_Type), "application/json");
testCase.verifyEqual(string(payload.headers.X_Test), "value");
testCase.verifyEqual(char(matlab.net.base64decode(payload.body)), '{"ok":true}');
end

function testBuildRawRequestWithoutQueryOrBody(testCase)
req.method = "GET";
req.path = "/status";
Expand Down
93 changes: 69 additions & 24 deletions toolbox/+mhs/+internal/GoSidecarTransport.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ function start(obj)
return;
end

mhs.internal.GoSidecarTransport.ensureBinaryExecutable(obj.BinaryPath);

pb = java.lang.ProcessBuilder({char(obj.BinaryPath), ...
'--port', char(string(obj.Port))});
pb.redirectErrorStream(true);
Expand Down Expand Up @@ -110,18 +112,15 @@ function writeResponse(obj, socket, responseBytes)
resp.id = char(socket.id);
resp.status = status;

% Convert dictionary to struct for jsonencode if needed
if isa(headers, 'dictionary')
hStruct = struct();
keys = headers.keys();
for i = 1:numel(keys)
field = matlab.lang.makeValidName(char(keys(i)));
hStruct.(field) = char(headers(keys(i)));
end
resp.headers = hStruct;
else
resp.headers = headers;
% parseResponseBytes always returns a dictionary; convert it to
% a struct because jsonencode does not support dictionary.
hStruct = struct();
keys = headers.keys();
for i = 1:numel(keys)
field = matlab.lang.makeValidName(char(keys(i)));
hStruct.(field) = char(headers(keys(i)));
end
resp.headers = hStruct;

resp.body = char(matlab.net.base64encode(body));

Expand Down Expand Up @@ -178,27 +177,73 @@ function onLineFromGo(obj, line)
% Locate the pre-compiled binary based on current platform.
toolboxRoot = fileparts(fileparts(fileparts( ...
mfilename('fullpath'))));
if ispc
path = fullfile(toolboxRoot, 'bin', 'win64', ...
path = mhs.internal.GoSidecarTransport.findBinaryForPlatform( ...
toolboxRoot, ispc, ismac, string(computer('arch')));
end

function ensureBinaryExecutable(path)
arguments
path (1,1) string
end

if ispc || ~isfile(path)
return;
end

[isOk, attributes] = fileattrib(path);
if isOk && isfield(attributes, "UserExecute") && attributes.UserExecute
return;
end

[status, output] = system(sprintf('chmod +x "%s"', char(path)));
if status ~= 0
error("MatlabHttpServer:BinaryPermissionDenied", ...
"Unable to mark Go sidecar binary as executable: %s", ...
strtrim(output));
end
end

function path = findBinaryForPlatform(toolboxRoot, isWindows, isMac, arch)
arguments
toolboxRoot (1,1) string
isWindows (1,1) logical
isMac (1,1) logical
arch (1,1) string
end

relativePath = mhs.internal.GoSidecarTransport ...
.binaryRelativePathForPlatform(isWindows, isMac, arch);
path = fullfile(toolboxRoot, relativePath);

if ~isfile(path)
error('MatlabHttpServer:binaryNotFound', ...
['Go sidecar binary not found: %s\n' ...
'Build with: cd sidecar && make build-all'], path);
end
end

function relativePath = binaryRelativePathForPlatform(isWindows, isMac, arch)
arguments
isWindows (1,1) logical
isMac (1,1) logical
arch (1,1) string
end

if isWindows
relativePath = fullfile('bin', 'win64', ...
'matlab-http-bridge.exe');
elseif ismac
if strcmp(computer('arch'), 'maca64')
path = fullfile(toolboxRoot, 'bin', 'maca64', ...
elseif isMac
if arch == "maca64"
relativePath = fullfile('bin', 'maca64', ...
'matlab-http-bridge');
else
path = fullfile(toolboxRoot, 'bin', 'maci64', ...
relativePath = fullfile('bin', 'maci64', ...
'matlab-http-bridge');
end
else
path = fullfile(toolboxRoot, 'bin', 'glnxa64', ...
relativePath = fullfile('bin', 'glnxa64', ...
'matlab-http-bridge');
end

if ~isfile(path)
error('MatlabHttpServer:binaryNotFound', ...
['Go sidecar binary not found: %s\n' ...
'Build with: cd sidecar && make build-all'], path);
end
end

function raw = buildRawRequest(req)
Expand Down
Loading