') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); mutest: new output arg for out-of-order cmd execution by liambrady · Pull Request #39 · LabNConsulting/munet · GitHub
Skip to content

mutest: new output arg for out-of-order cmd execution - #39

Draft
liambrady wants to merge 1 commit into
LabNConsulting:mainfrom
liambrady:liambrady/step-no-output
Draft

mutest: new output arg for out-of-order cmd execution#39
liambrady wants to merge 1 commit into
LabNConsulting:mainfrom
liambrady:liambrady/step-no-output

Conversation

@liambrady

@liambradyliambrady commented Oct 23, 2024

Copy link
Copy Markdown
Contributor

Adds a new argument, output: bool, for the mutest method step() that allows for a cmd to be executed within a node without waiting for any output. This introduces the ability to effectively run a series of (possibly lengthy) commands in the background while the mutest is allowed to continue executing further steps (some of which may require a previous command to still be in the middle of executing).

This commit also modifies _cmd_status() in base.py to support executing a command without returning output in the first place.

@liambradyliambrady added enhancement New feature or request mutest mutest related item labels Oct 23, 2024
@codecov

codecovBot commented Oct 23, 2024

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.33%. Comparing base (f0447ca) to head (67b3716).
⚠️ Report is 183 commits behind head on main.

Files with missing linesPatch %Lines
munet/base.py60.00%2 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #39 +/- ##
==========================================
+ Coverage 58.94% 59.33% +0.38% 
==========================================
Files 18 19 +1 Lines 5286 5545 +259 ==========================================
+ Hits 3116 3290 +174 - Misses 2170 2255 +85 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment threadmunet/base.py
else:
o = ''
e = ''
return self._cmd_status_finish(p, cmds, actual_cmd, o, e, raises, warn)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is expecting to run after the process has completed (one side effect of the p.communicate call above is that it waits until this happens). If we look at _cmd_status_finish you'll see it checks p.returncode as a boolean (expecting 0 to mean success); however, in the not-completed-running case p.returncode will be None so this also looks like success.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured that in the case that an immediate error occurred within the process, then maybe catching it isn't a bad thing. In retrospect though, leaving that up to chance is probably a bad idea and consistency would be preferred so I will probably modify this to skip the _cmd_status_finish entirely.

Comment threadmunet/base.py
raises=False,
warn=True,
stdin=None,
output=True,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand you're looking for a "fire-and-forget" command, I just wonder if this is the right way to do that.

I think that perhaps instead of output the new param should be no_wait=False.

I'm also wondering if we should explicitly set stdout=subprocess.DEVNULL and stderr=subprocess.DEVNULL.

target: str,
cmd: str,
output: bool = True,
) -> str:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general creating these fire-and-forget processes is messy b/c there's no guarantee they will complete before the test exists (in which case they will probably be killed by the kernel, or have PIPE closed signals or something). For non-mutest uses one uses popen to start processes that you want to run in the background and you get back a p process object that you can kill or wait on later. I don't know if this is the right pattern to use for mutest though -- so maybe your way here is ok.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder about the actual use case. Might a new API that runs multiple commands simultaneously and waits for them all to complete would be a cleaner solution (step_multi or step_parallel)? It depends on the problem we're trying to solve I guess.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use case I have encountered twice now is the desire to create some sort of traffic generator on a node (or two nodes in the case of setting up a client/server pair situation) and then run a series of tests based on the results (perhaps on separate nodes). While it would probably be feasible to do this with some sort of new waitstep_multi or waitstep_parallel, such would require that API call to be responsible for multiple parallel steps plus multiple waiting steps which seems excessive. I figured that accepting the risk and running a few fire-and-forget cmd through a step (then using match/wait calls as usual but written by the tester to be robust to the state of the background process) was cleanest.

I do agree that letting the kernel kill the processes is a naive solution though, so perhaps the best solution would be to keep track of all fire-and-forget processes in a list and clean it up later when a node is being deleted?

@liambrady
liambrady marked this pull request as draft November 23, 2024 19:25
@liambrady

Copy link
Copy Markdown
ContributorAuthor

There is a work around to this issue that I recently have seen others take advantage of. In order to run a step without mutest waiting for it, you can run a command in the background of a subshell (e.g. forcing the background process to get orphaned.)

e.g. step('r1', 'bash -c "ping 192.168.0.1 &"')

This runs into the same concerns discussed earlier in this thread, as a user must make sure to manually kill whatever background process they started before concluding the mutest.

@choppsv1
choppsv1force-pushed the main branch 3 times, most recently from 542f956 to d3bfa70CompareJanuary 26, 2026 08:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requestmutestmutest related item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liambrady@choppsv1