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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.13] - 2026-07-28

### Added
- Support for discovering and running MATLAB unit tests (Addresses [mathworks/MATLAB-extension-for-vscode#138](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/138))
- Improved logic for launching MATLAB on macOS systems

### Fixed
- Resolves errors that occur when the language server sends requests for features not supported by the client (Addresses [mathworks/MATLAB-language-server#88](https://github.com/mathworks/MATLAB-language-server/issues/88))

## [1.3.12] - 2026-06-15

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

MATLAB® language server implements the Microsoft® [Language Server Protocol](https://github.com/Microsoft/language-server-protocol) for the MATLAB language.

MATLAB language server requires MATLAB version R2021b or later.
MATLAB language server requires MATLAB version R2021b or later. Run the language server only in trusted workspaces.

## Features Implemented
MATLAB language server implements several Language Server Protocol features and their related services:
Expand Down
102 changes: 102 additions & 0 deletions matlab/+matlabls/+handlers/+testing/TestStreamingPlugin.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
classdef TestStreamingPlugin < matlab.unittest.plugins.TestRunnerPlugin
% Copyright 2026 The MathWorks, Inc.

properties (Access=private)
ResponseChannel string
end

methods
function plugin = TestStreamingPlugin(responseChannel)
plugin.ResponseChannel = responseChannel;
end
end

methods (Access=protected)
function runTest(plugin, pluginData)
startEvent.type = 'started';
startEvent.testName = char(pluginData.Name);
startEvent.testFile = getTestFile(pluginData);
plugin.publishEvent(startEvent);

runTest@matlab.unittest.plugins.TestRunnerPlugin(plugin, pluginData);
end

function reportFinalizedResult(plugin, pluginData)
testResult = pluginData.TestResult;

event.type = 'finished';
event.testName = char(pluginData.Name);
event.testFile = getTestFile(pluginData);
event.status = getStatusString(testResult);
event.duration = testResult.Duration;
event.diagnostics = extractDiagnostics(testResult);

plugin.publishEvent(event);

reportFinalizedResult@matlab.unittest.plugins.TestRunnerPlugin(plugin, pluginData);
end

function publishEvent(plugin, event)
matlabls.internal.CommunicationManager.publish(plugin.ResponseChannel, event);
end
end
end


function testFile = getTestFile(pluginData)
ts = pluginData.TestSuite;
tc = ts.TestClass;
if strlength(tc) > 0
testFile = fullfile(char(ts.BaseFolder), [char(tc) '.m']);
else
parts = strsplit(char(pluginData.Name), '/');
testFile = fullfile(char(ts.BaseFolder), [parts{1} '.m']);
end
end


function status = getStatusString(result)
if result.Passed
status = 'passed';
elseif result.Failed
status = 'failed';
else
status = 'incomplete';
end
end


function diags = extractDiagnostics(result)
diags = {};
if result.Passed
return;
end

details = result.Details;
if ~isfield(details, 'DiagnosticRecord') || isempty(details.DiagnosticRecord)
return;
end

records = details.DiagnosticRecord;
diags = cell(1, numel(records));
for i = 1:numel(records)
d.message = char(records(i).Report);
if ~isempty(records(i).Stack)
d.failedInFile = char(records(i).Stack(1).file);
d.failedOnLine = records(i).Stack(1).line;
stackFrames = cell(1, numel(records(i).Stack));
for j = 1:numel(records(i).Stack)
frame.file = char(records(i).Stack(j).file);
frame.name = char(records(i).Stack(j).name);
frame.line = records(i).Stack(j).line;
stackFrames{j} = frame;
end
d.stack = stackFrames;
else
d.failedInFile = '';
d.failedOnLine = 0;
d.stack = {};
end
diags{i} = d;
end
end
28 changes: 28 additions & 0 deletions matlab/+matlabls/+handlers/+testing/TextOutputStream.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
classdef TextOutputStream < matlab.unittest.plugins.OutputStream
% Publishes printed text to a channel for streaming to VS Code.

% Copyright 2026 The MathWorks, Inc.

properties (Access=private)
ResponseChannel string
end

methods
function stream = TextOutputStream(responseChannel)
stream.ResponseChannel = responseChannel;
end

function print(stream, formatSpec, varargin)
text = sprintf(formatSpec, varargin{:});
event.type = 'output';
event.text = text;
stream.publishEvent(event);
end
end

methods (Access=protected)
function publishEvent(stream, event)
matlabls.internal.CommunicationManager.publish(stream.ResponseChannel, event);
end
end
end
79 changes: 79 additions & 0 deletions matlab/+matlabls/+handlers/+testing/discoverTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
function result = discoverTests(paths, mode)
%DISCOVERTESTS Discover MATLAB unit tests from files or folders.
% paths - string array of file/folder paths
% mode - 'file' or 'folder'
%
% Returns a struct with fields:
% names - cell array of test name strings
% filenames - cell array of file path strings
% procedureNames - cell array of method name strings
% testParentNames - cell array of class name strings
% parameterizations - cell array of parameterization strings
% error - '' on success, error message on failure
% warning - '' if no warnings, otherwise warning text from MATLAB

% Copyright 2026 The MathWorks, Inc.

try
lastwarn('');
suites = cell(1, numel(paths));
for i = 1:numel(paths)
if strcmp(mode, 'folder')
suites{i} = matlab.unittest.TestSuite.fromFolder(paths{i}, ...
'IncludingSubfolders', true);
else
suites{i} = matlab.unittest.TestSuite.fromFile(paths{i});
end
end
suite = [suites{:}];
warnMsg = lastwarn;
warnMsg = regexprep(warnMsg, '<a[^>]*>', '');
warnMsg = strrep(warnMsg, '</a>', '');

n = numel(suite);
names = cell(1, n);
filenames = cell(1, n);
procedureNames = cell(1, n);
testParentNames = cell(1, n);
parameterizations = cell(1, n);

for i = 1:n
fullName = char(suite(i).Name);
names{i} = fullName;
procedureNames{i} = char(suite(i).ProcedureName);

tc = suite(i).TestClass;
if strlength(tc) > 0
testParentNames{i} = char(tc);
filenames{i} = fullfile(char(suite(i).BaseFolder), [char(tc) '.m']);
else
parts = strsplit(fullName, '/');
testParentNames{i} = parts{1};
filenames{i} = fullfile(char(suite(i).BaseFolder), [parts{1} '.m']);
end

parenIdx = strfind(fullName, '(');
if ~isempty(parenIdx)
parameterizations{i} = fullName(parenIdx(1)+1:end-1);
else
parameterizations{i} = '';
end
end

result.names = names;
result.filenames = filenames;
result.procedureNames = procedureNames;
result.testParentNames = testParentNames;
result.parameterizations = parameterizations;
result.error = '';
result.warning = warnMsg;
catch ME
result.names = {};
result.filenames = {};
result.procedureNames = {};
result.testParentNames = {};
result.parameterizations = {};
result.error = ME.message;
result.warning = '';
end
end
47 changes: 47 additions & 0 deletions matlab/+matlabls/+handlers/+testing/runTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
function runTests(testFiles, testNames, responseChannel)
%RUNTESTS Run MATLAB unit tests with streaming results.
% testFiles - cell array of absolute file paths
% testNames - cell array of specific test names (empty = run all)
% responseChannel - Faye channel for publishing per-test events

% Copyright 2026 The MathWorks, Inc.

try
suites = cell(1, numel(testFiles));
for i = 1:numel(testFiles)
try
suites{i} = matlab.unittest.TestSuite.fromFile(testFiles{i});
catch
suites{i} = matlab.unittest.TestSuite.empty;
end
end
suite = [suites{:}];

% Filter to specific test names if provided
if ~isempty(testNames)
mask = ismember({suite.Name}, testNames);
suite = suite(mask);
end

if isempty(suite)
completeEvent.type = 'complete';
matlabls.internal.CommunicationManager.publish(responseChannel, completeEvent);
return;
end

% Create runner with diagnostics recording and streaming plugin
runner = matlab.unittest.TestRunner.withNoPlugins();
runner.addPlugin(matlab.unittest.plugins.DiagnosticsRecordingPlugin);
runner.addPlugin(matlabls.handlers.testing.TestStreamingPlugin(responseChannel));
outputStream = matlabls.handlers.testing.TextOutputStream(responseChannel);
runner.addPlugin(matlab.unittest.plugins.TestRunProgressPlugin.withVerbosity(2, outputStream));
runner.addPlugin(matlab.unittest.plugins.DiagnosticsOutputPlugin(outputStream));

runner.run(suite);
catch
% Interrupted or unexpected error — fall through to complete event
end

completeEvent.type = 'complete';
matlabls.internal.CommunicationManager.publish(responseChannel, completeEvent);
end
Loading
Loading