Last active
August 14, 2025 08:02
-
-
Save gdotdesign/c678136cddbb8ed66b0eb4845191ec7e to your computer and use it in GitHub Desktop.
Auto bisect files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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