-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
initial --watch mode #2
Conversation
Hi! I hope you don't mind that I'm watching the project. Could you elaborate on this limitation? The default QUnit CLI has a built-in watch mode nowadays. Feel free to ignore my question if it would take from your momentum. I think qunitx I is about exploring new territory, so my question is not important. But know that I'm happy to answer any of your questions, and I don't mind making changes in core if needed. Thanks! |
Hi @Krinkle ! Thanks for your response! I was actually going to create an issue on qunit side for this, you've acted earlier than I did :) This issue is complicated, I did check the cli code and most of the qunit code. The problem is Problem #1(internal QUnit state)There are specific states that gets persisted inside the // on qunit/qunit.js#3453
// ....
only: test.only,
stop: function stop() { // NOTE: instead probably we want a public window.QUnit.reset() that also resets internal test suite state
config.autostart = false;
globalStartCalled = false;
runStarted = false;
},
start: function start(count) {
if (config.current) {
throw new Error("QUnit.start cannot be called inside a test context.");
}
// ....
} This allows running after file watch in node.js let initialConfigCache = deepcopy(window.QUnit.config)'
// ...
window.QUnit.config = initialConfigCache;
// ...
window.QUnit.start(); Even this doesnt work, so clearning window.QUnit.config.queue or any config related state itself isnt enough, some state still gets persisted after the test suite finishes it seems. Therefore if we introduce a Problem #2(Why cli cache reset approach didnt work):In addition I spent many hours trying to clean the import { createRequire } from 'module';
export function runTests(testFiles) {
const require = createRequire(import.meta.url);
// ....
Object.keys(require.cache).forEach((module) => delete require.cache[module]);
// Then import the test modules
window.Qunit.start();
} Even this and many variants of this(completely reseting window.QUnit/reloading etc) doesnt work with ESM modules. I couldnt get it to work. I had seen you do something like these here afterward by coincidence as well during my reads :) https://github.com/qunitjs/qunit/blob/main/src/cli/run.js#L145 In other words I think we need in-memory QUnit resets/restarts, with something like this public API:
Not only this would be faster, less hacky and error prone & less node.js specific, it would also probably cleanup the internals even further by reorganizing the program state. Let me know if I missed anything, I'd love to implement this feature and already spent many days trying many different approaches as you can see :) |
Hi @Krinkle , I've spent few more hours today trying to do this in memory, the thing is we still need ESM cache busting even if we do things in memory because the files are watched and reloaded in node.js context. I'm 100% sure cache busting doesnt work on ESM modules at the moment. I hope node.js will allow busting the ESM cache. Then I can implement --watch mode for node.js, currently it works in this MR for Meanwhile we could add reset: function stop() {
config = window.QUnit.config;
config.queue.length = 0;
config.modules.length = 0;
config.currentModule = {
name: "",
tests: [],
childModules: [],
testsRun: 0,
testsIgnored: 0,
hooks: {
before: [],
beforeEach: [],
afterEach: [],
after: []
}
};
globalSuite.childSuites.length = 0;
delete globalSuite._startTime;
delete globalSuite._endTime;
config.currentModule.suiteReport = globalSuite;
config.modules.push(config.currentModule);
delete config.stats;
delete config.started;
delete config.updateRate;
delete config.filter;
delete config.depth;
delete config.current;
delete config.pageLoaded;
delete config.timeoutHandler;
delete config.timeout;
delete config.pollution;
ProcessingQueue.finished = false;
config.autostart = false;
globalStartCalled = false;
runStarted = false;
if (typeof config !== 'undefined') {
// browser dist config refers to the internal config variable instead of window.QUnit.config unless assigned explicitly with
// var config = window.QUnit.config;
}
}, |
@Krinkle apparently |
@Krinkle I was able to make it work only once I can call reset: function reset() {
ProcessingQueue.finished = false;
globalStartCalled = false;
runStarted = false;
let QUnitConfig = window.QUnit.config;
QUnitConfig.queue.length = 0;
QUnitConfig.modules.length = 0;
QUnitConfig.autostart = false;
['stats', 'started', 'updateRate', 'filter', 'depth', 'current', 'pageLoaded', 'timeoutHandler', 'timeout', 'pollution']
.forEach((key) => delete QUnitConfig[key]);
let suiteReport = QUnitConfig.currentModule.suiteReport;
suiteReport.childSuites.length = 0;
delete suiteReport._startTime;
delete suiteReport._endTime;
QUnitConfig.modules.push(QUnitConfig.currentModule);
},
start: function start(count) {
// ... normal start definition |
@izelnakri Hey, sorry for taking a while. I've been thinking about this and not had the time to properly sit down longer for this. I think the QUnit CLI's current approach is not an example I'd recommend following. I didn't think about how it currently works when I referred to it. Looking at it now, it feels rather fragile to me. I think the only reason we currently do this is perhaps because our past selves thought it'd be marginally faster than spawning a sub process. Or that maybe we didn't yet want to deal with untangling certain parts of the CLI code. Continuing to share global state feels wrong to me.
So, yeah, I think the common approach that other frameworks follow (e.g. tape, Karma, airtap, etc.) is to use a fresh process in Node, and a fresh tab in the browser. If we're worried about latency, we can for example spawn the "next" process directly after a run finishes so that we're primed and ready to go for the next run. |
Hi @Krinkle, I appreciate your thoughtful responses, better late than never :) I hacked qunit/made --watch mode and 2 modes(node & browser) working for qunitx. Here is how I achieved it:
=============================
Lastly, I think we shouldn't completely follow other JS testing tools, I have an extensive experience with most of them and none of them got it fully right I think, particularly the concurrency story even including |
@izelnakri Thanks, I love all the work you're doing here. It's a fresh look (but informed by experience) at many things, and I think it's exactly what we need. I do still feel a bit skeptical on re-running tests within the same Node or browser tab. Could you try to sell me on why/how it beneficial to developers for us to do this internally, compared to spawning a fresh process. It seems like that would simplify things and categorically remove a significant source of possible bugs and confusions. I care about speed, a lot, but I have not previously found the cost of one process or tab to be significant in that way. We could even spawn these ahead of time when in watch mode, so that it has the bridge and test runner already warmed up and ready to go. This in addition to the general latency tolerance we have by running in the background on file changes (e.g. before one decides to look for the results etc). I can imagine there being cases where purely the loading of the application source code takes a significant amount of time (the test runner can load ahead of time, and the test suites can be filtered as you do, but app source code is tricky indeed because we can't load that until we know what has changed). Is this the scenario that motivated you in this direction? Or is it somethin else? Do you have some data or examples I could look at to better understand why it takes as long as it does? If through some some magic we could reduce that time to within a duration you like, would that change your mind? PS: We can continue to work side-by-side for a while too, for example, we could land and release https://github.com/qunitjs/qunit/pull/1598/files as an experimental feature first, and then you can start reducing complexity on this side. The only thing I ask is a couple of tests in that case so that it's easier for me not to accidentally break it in refactoring. |
Hi @Krinkle Im glad to see you're very interested in the experiment :) So far I've been able to succeed with my needs however as you might seen, I had to tweak/hack qunit to accomplish this, qunitx currently runs a vendored qunit to be able to clear the complete tests state on reruns. This is the only way I found that allowed Qunit.on events to be run(for tap format output on done, early --failFast on testDone, being able to kill a running qunit on keyboard shortcuts). I suppose some of it could be done through ipc, however right now I accomplish this state reset/small mutation in my vendored/hacked qunit with almost no code on qunitx, it just makes an JS 'import()' with querystring to bust the es module cache. I see the reason why you're hesitant to rewrite the process spawning logic, since much of the code is already written. However I find my approach to be simpler since it requires very little changes and no code for ipc(there just the websocket connection only during --browser mode to execute a function on the puppeteer instance). As I see it 'qunit' creates and maintains its state the cli runner shouldnt change it ideally. Lastly and perhaps the most importantly, spawning a new process creates its own memory/state thus making state management even more difficult. For example, recently I introduced --before and --after script loading feature which allows qunitx users to introduce or change the global state before a test suite runs. First this seemed like a bad idea, then I realized it is a requirement in certain cases like when I want to run my pretender extension(@memserver/server) to be run on qunitx node or --browser mode interchangably, with 0 changes on test files, which is quite remarkable/amazing. Qunitx allows me to tests universal javascript libraries, for node tests I introduced a mocked dom for node.js mode with 'jsdom' before the test suite runs. This was quite possibly the first time it ever happened in JS ecosystem, 'pretender' even says it doesnt support node.js but I was able to hack it. So in summary, I preferred handling test runtime in a single node.js process(when its not viewed in browser, thats seperate): 1- Existing in-process approach doesnt require process spawning, ipc, stdout management and process supervising. Just a node.js es import and existing qunit event registration code. ======= Also apologies for not introducing any tests for my proposed qunit code changes, I found it currently difficult to test the qunit internal state as its hidden behind the 'this' context, we might need to do significant changes to the existing tests and internal state management to be able to cover this fully with tests, thats why omitted them, I really wanted to include few tests on the qunit side ;) As you might have seen I also didn't fully cover the qunitx source code as I found this specific code to not worth the effort cost/benefit wise, unless it gets significant usage/requires maturity/stableness during releases. |
No description provided.