Skip to content

Instantly share code, notes, and snippets.

@gdotdesign
Last active August 14, 2025 08:02
Show Gist options
  • Save gdotdesign/c678136cddbb8ed66b0eb4845191ec7e to your computer and use it in GitHub Desktop.
Save gdotdesign/c678136cddbb8ed66b0eb4845191ec7e to your computer and use it in GitHub Desktop.
Auto bisect files.
.PHONY: build
build: bin/mint
.PHONY: spec
spec:
crystal spec --error-on-warnings --error-trace --progress
.PHONY: spec-cli
spec-cli: build
crystal spec spec_cli/*_spec.cr spec_cli/**/*_spec.cr --error-on-warnings --error-trace --progress
.PHONY: format
format:
crystal tool format
.PHONY: format-core
format-core: build
cd core && ../bin/mint format
cd core/tests && ../../bin/mint format
.PHONY: ameba
ameba:
bin/ameba
.PHONY: test
test: spec ameba
.PHONY: test-core
test-core: build
cd core/tests && ../../bin/mint test -b chrome
.PHONY: development
development: build
mv bin/mint ~/.bin/mint-dev
.PHONY: local
local: build
mv bin/mint ~/.bin/mint
.PHONY: documentation
documentation:
rm -rf docs && crystal docs
.PHONY: development-release
development-release:
docker-compose run --rm app \
crystal build src/mint.cr -o mint-dev --static --no-debug --release
mv ./mint-dev ~/.bin/
src/assets/runtime.js: \
$(shell find runtime/src -type f) \
runtime/index.js
cd runtime && make index
src/assets/runtime_test.js: \
$(shell find runtime/src -type f) \
runtime/index_testing.js \
runtime/index.js
cd runtime && make index_testing
# This builds the binary and depends on files in some directories.
bin/mint: \
$(shell find core/source -type f) \
$(shell find runtime/src -type f) \
$(shell find src -type f) \
src/assets/runtime_test.js \
src/assets/runtime.js
~/Lib/crystal/bin/crystal build src/mint.cr -p -o bin/mint
#!/bin/bash
# This needs to run in the Crystal source directory. In my case it was in `/home/gus/Lib/crystal`.
set -euo pipefail
# I've build using the pre-compiled 1.16.3 binary for all tests
rm -rf .build && make
# Build mint using the the compiler (Makefile needs to be patched).
(cd /home/gus/Projects/mint-lang/mint && make development)
# Run the actual test.
node tester.js
#!/usr/bin/env node
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const problematicContent = `
component Main {
fun render : String {
<div/>
}
}
`;
class LSPTester {
constructor(options = {}) {
this.clientBinary = options.clientBinary || process.argv[2] || '/home/gus/.bin/mint-dev';
this.outputFile = options.outputFile || process.argv[3] || 'lsp_output.json';
this.waitTime = parseInt(options.waitTime || process.argv[4] || '5') * 1000; // Convert to ms
this.outputBuffer = '';
this.client = null;
this.tempWorkspace = null;
this.workspaceUri = null;
this.testFileUri = null;
}
log(message) {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
console.error(`[${timestamp}] ${message}`);
}
async createTempWorkspace() {
// Create temporary workspace directory
this.tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'lsp-test-workspace-'));
this.workspaceUri = `file://${this.tempWorkspace}`;
this.log(`Created temporary workspace: ${this.tempWorkspace}`);
// Create mint.json configuration file
const mintConfig = {
"source-directories": ["src"]
};
const mintJsonPath = path.join(this.tempWorkspace, 'mint.json');
fs.writeFileSync(mintJsonPath, JSON.stringify(mintConfig, null, 2));
this.log(`Created mint.json at: ${mintJsonPath}`);
// Create src directory and test files
const srcDir = path.join(this.tempWorkspace, 'src');
fs.mkdirSync(srcDir, { recursive: true });
// Create main test file
const testFilePath = path.join(srcDir, 'Main.mint');
const testFileContent = `
component Main {
fun render : Html {
<div/>
}
}
`;
fs.writeFileSync(testFilePath, testFileContent);
this.testFilePath = testFilePath;
this.testFileUri = `file://${testFilePath}`;
this.log(`Created test file: ${testFilePath}`);
this.log(`Workspace structure created with ${fs.readdirSync(this.tempWorkspace).length} files`);
}
cleanupTempWorkspace() {
if (this.tempWorkspace && fs.existsSync(this.tempWorkspace)) {
try {
fs.rmSync(this.tempWorkspace, { recursive: true, force: true });
this.log(`Cleaned up temporary workspace: ${this.tempWorkspace}`);
} catch (error) {
this.log(`Warning: Failed to clean up workspace: ${error.message}`);
}
}
}
createInitializeMessage() {
const message = {
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
processId: process.pid,
capabilities: {
workspace: {
workspaceFolders: true,
configuration: true,
didChangeConfiguration: {
dynamicRegistration: true
}
},
textDocument: {
publishDiagnostics: {
relatedInformation: true,
versionSupport: true,
codeDescriptionSupport: true,
dataSupport: true
},
synchronization: {
willSave: true,
willSaveWaitUntil: true,
didSave: true
}
}
}
}
};
return this.formatLSPMessage(message);
}
createInitializedMessage() {
const message = {
jsonrpc: "2.0",
method: "initialized",
params: {}
};
return this.formatLSPMessage(message);
}
createDidOpenMessage() {
const content = fs.readFileSync(this.testFileUri.replace('file://', ''), 'utf8');
const message = {
jsonrpc: "2.0",
method: "textDocument/didOpen",
params: {
textDocument: {
uri: this.testFileUri,
languageId: "mint",
version: 1,
text: content
}
}
};
return this.formatLSPMessage(message);
}
createDidChangeMessage() {
const message = {
jsonrpc: "2.0",
method: "textDocument/didChange",
params: {
textDocument: {
uri: this.testFileUri,
},
contentChanges: [{
text: problematicContent
}]
}
};
return this.formatLSPMessage(message);
}
formatLSPMessage(message) {
const jsonString = JSON.stringify(message);
const contentLength = Buffer.byteLength(jsonString, 'utf8');
return `Content-Length: ${contentLength}\r\n\r\n${jsonString}`;
}
async checkBinaryExists() {
return new Promise((resolve) => {
fs.access(this.clientBinary, fs.constants.F_OK | fs.constants.X_OK, (err) => {
resolve(!err);
});
});
}
async runTest() {
this.log('Starting language server client test...');
this.log(`LSP Client Binary: ${this.clientBinary}`);
this.log(`Output File: ${this.outputFile}`);
this.log(`Wait Time: ${this.waitTime / 1000}s`);
// Create temporary workspace
await this.createTempWorkspace();
// Check if binary exists
const binaryExists = await this.checkBinaryExists();
if (!binaryExists) {
this.log(`ERROR: Language server client binary '${this.clientBinary}' not found or not executable`);
this.log('Usage: node lsp_test.js [lsp_client_binary] [output_file] [wait_time_seconds]');
this.cleanupTempWorkspace();
process.exit(1);
}
// Prepare messages
const messages = [
this.createInitializeMessage(),
this.createInitializedMessage(),
this.createDidOpenMessage(),
// Add a delay and then send didChange to trigger more diagnostics
this.createDidChangeMessage()
];
this.log('Prepared LSP messages:');
this.log(` Workspace URI: ${this.workspaceUri}`);
this.log(` Test file URI: ${this.testFileUri}`);
messages.forEach((msg, i) => {
const jsonPart = msg.split('\r\n\r\n')[1];
const parsed = JSON.parse(jsonPart);
this.log(` Message ${i + 1} (${parsed.method || `${parsed.method} id:${parsed.id}`}): ${jsonPart.substring(0, 100)}...`);
});
return new Promise((resolve, reject) => {
// Start the language server client
this.log('Starting LSP client and sending messages...');
this.client = spawn(this.clientBinary, ["tool", "ls"], {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: this.tempWorkspace // Set working directory to the workspace
});
let hasEnded = false;
// Handle client output
this.client.stdout.on('data', (data) => {
this.outputBuffer += data.toString();
});
this.client.stderr.on('data', (data) => {
this.outputBuffer += data.toString();
});
// Handle client process events
this.client.on('error', (error) => {
if (!hasEnded) {
hasEnded = true;
this.log(`ERROR: Failed to start LSP client: ${error.message}`);
this.cleanupTempWorkspace();
reject(error);
}
});
this.client.on('close', (code, signal) => {
if (!hasEnded) {
this.log(`LSP client process ended with code ${code} and signal ${signal}`);
}
});
// Send messages with delays
this.sendMessagesSequentially(messages)
.then(() => {
fs.writeFileSync(this.testFilePath, problematicContent);
this.log(`Waiting ${this.waitTime / 1000} seconds for responses...`);
// Wait for responses
setTimeout(() => {
hasEnded = true;
this.finishTest(resolve);
}, this.waitTime);
})
.catch((error) => {
hasEnded = true;
this.cleanupTempWorkspace();
reject(error);
});
});
}
async sendMessagesSequentially(messages) {
for (let i = 0; i < messages.length; i++) {
await this.sendMessage(messages[i]);
// Longer delay between messages to give LSP time to process
await new Promise(resolve => setTimeout(resolve, 500));
}
}
sendMessage(message) {
return new Promise((resolve, reject) => {
if (!this.client || this.client.killed) {
reject(new Error('LSP client is not running'));
return;
}
this.client.stdin.write(message, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
finishTest(resolve) {
// Terminate the client process
if (this.client && !this.client.killed) {
this.log('Terminating LSP client process...');
this.client.kill('SIGTERM');
// Force kill after 2 seconds if it doesn't respond
setTimeout(() => {
if (!this.client.killed) {
this.client.kill('SIGKILL');
}
}, 2000);
}
// Write output to file
fs.writeFileSync(this.outputFile, this.outputBuffer);
this.log(`Output captured to: ${this.outputFile}`);
// Check for publishDiagnostics
const success = this.checkForPublishDiagnostics();
// Cleanup workspace
this.cleanupTempWorkspace();
resolve(success);
}
checkForPublishDiagnostics() {
if (this.outputBuffer.includes('publishDiagnostics')) {
this.log('✅ SUCCESS: publishDiagnostics notification found in output!');
return true;
} else {
this.log('❌ FAILURE: publishDiagnostics notification NOT found in output');
// Show what we did capture
this.log('Captured output:');
const lines = this.outputBuffer.split('\n').slice(0, 20);
lines.forEach(line => {
if (line.trim()) {
this.log(` ${line}`);
}
});
if (this.outputBuffer.split('\n').length > 20) {
this.log(` ... (output truncated, see ${this.outputFile} for full content)`);
}
return false;
}
}
}
// Main execution
async function main() {
const tester = new LSPTester();
try {
const success = await tester.runTest();
process.exit(success ? 0 : 1);
} catch (error) {
tester.log(`ERROR: ${error.message}`);
if (tester.tempWorkspace) {
tester.cleanupTempWorkspace();
}
process.exit(1);
}
}
// Handle process termination
process.on('SIGINT', () => {
console.error('\nReceived SIGINT, cleaning up...');
process.exit(1);
});
process.on('SIGTERM', () => {
console.error('\nReceived SIGTERM, cleaning up...');
process.exit(1);
});
if (require.main === module) {
main();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment