Rethinking software testing: perspectives from the world of hardware (2019)
software.rajivprab.com
software.rajivprab.com
With consumer software, there are no stakes. The business knows it can make money with buggy software. The developers know the business doesn't care. In fact, most people in the business don't seem to care if it works at all. The only time they care is if there is a legal consequence, or if they will get fired. They are not always stupid, but they are always lazy.
And that is never going to change. There are parts of this world that are perfectly acceptable as just being shitty, and at least 95% of software in the world fits into that. Things don't get better without a motivation.
Not necessarily, but the cure could be worse than the disease.
The problem is that the stakes are getting higher and higher for all software. Every app developer is trying to turn their program into a PID collector, and they are storing the PID in leaky buckets.
I mean, why would I get an account setup screen to play a solitaire game? Sheesh.
But politicians and bureaucracies have a nasty habit of imposing fairly ridiculous and/or out-of-date remedies.
They could make it a requirement to fax your PID to play solitaire.
Business do care it is just much more difficult than people realize to make software simple and easy to use. Then you have the fact most software projects fail and way over budget so they think there is no other way. Therefore they end up settling since at least they got something that sort of works which is better than nothing.
Nonetheless, I do agree your assertion that "Stakes are 100% the reason most tech is crap.". As a result of the limitation mentioned earlier, there must be some trade off or you can never wrap up the project. Stakes just make things worse and they tends to push engineers to cut corners in pursuing profits. The only thing looks better for hardware is that if you went too far cutting corners, it will not work stably at best or even does not work at all. For software, defects can easily be covered up and the end user might not have noticed it in the first place.
Why not? Intel shipped buggy hardware to everyone for years, it was called "Meltdown".
I'm fairly familiar with this conundrum.
The single biggest issue that hardware folks seem to have with software, is refusal to accept that software, in order to be valuable, has to run by different rules from hardware.
They. Just. Can't. Accept. That.
If it works for hardware, it must work for software. Attempts to explain that the agility inherent in software is a huge advantage, are met with stony glares, and accusations that "programmers are lazy slobs that hate quality."
So software developers are sidelined, treated like crap, not supported in efforts to achieve quality processes (it must be the same as hardware, or it's "lazy slob" stuff), insulted, tripped up, and basically ridiculed. It totally sucks.
Software is different from hardware; even firmware is different, these days. That simple assertion needs to be the baseline for any true reconciliation.
But I agree that modern software is of appallingly bad quality, and there's a dearth of lessons learned/mentoring in Quality practices.
Most practices (like TDD) are useful, but still fairly nascent, and tend to be "one-trick ponies." They will work nicely for some projects, but can be disasters for others.
Quality is a continuum; not a point. In hardware, we can get away with treating it as a point, but we hobble software, when we try to do it the hardware way.
Since leaving my last job, I have been working with ideas I developed during my career, that were never supported by the companies I worked for. These ideas are working fairly well, but they won't work for everyone.
Except for the moment where the hardware contains a mistake, then it's on the software to be flexible and contort to somehow workaround the hardware issue, because software is magic and can adapt to everything.
That said, I see plenty companies that are not as bad, actively work to change or at least accept that they don't "get" software entirely and that they get better results if they leave it to those who do.
And also agreed in the other direction: Lots of resources about building software today describe things that work for a SaaS product you can deploy 5x a day. You can't do that if you ship hardware, especially hardware that can't phone home daily for a software fix.
Integration tests usually seem to find the most important bugs, but on hardware they take hours or days to run. With that limitation removed in software development, they're almost too good to be true.
I'd also be interested in seeing how formal verification could be applied to software development.
To give concrete examples:
Intel knows what it needs to accomplish in order to win back the top spot. There are tons of challenges to get there, but the end goal is clear. That’s an entirely different kind of problem from Twitter needing to figure how to monitize.
It's afaik the most complex piece of software with full formal verification to date.
I've heard that ACL2 is also a thing that exists.
To me, one of the most intimidating parts of getting into formal verification is deciding which system to use - as you mentioned, there are several. What are the strengths and weaknesses of each?
I think the big choice is between using lightweight or heavy tools. Things like Coq and Agda take a lot of time to learn and implement but can derive 'correctly Haskell code which you can use, while TLA+ is more like a blueprint tool to check an algorithm before you write it in {favourite_language}. I think it's practical to learn TLA+ but if it's purely for intellectual satisfaction Wadler has a good book called Programming Foundations in Agda (I think?) which is available for free online.
However, in my opinion, it is still overly lax: The aim should always be complete coverage.
The article hints at this with:
“Test every combination of events? Test every output for every combination of events? Test every possible corner case? You would need an absolutely enormous number of tests in order to achieve that!”
However, it does not really follow through on it, and even continues with a dedicated section on "Why Random Testing Works". At least symbolic execution with coarse approximations should be used to cover all cases subsumed by more abstract domains.
The reason for this is that the number of possible cases is usually so large, and the areas where problems lie often so small, that random tests are unlikely to discover all mistakes. The article mentions precisely this situation: "The bug only manifested itself during a small set of overlapping corner cases." QuickCheck and fuzzing are popular and do find mistakes in practice. However, how many mistakes are not found by these tools, if such random approaches already discover mistakes?
In my opinion, an ideal test case only terminates if a mistake is found. If not, it will simply keep on searching, potentially for months, years, ... You need to be able to start the search for mistakes and keep it running for as long as you want.
Logic programming languages like Prolog excel at writing test cases, because they provide search as an implicit language feature. It is easy to write a query that asks: "Are there cases where such-and-such holds?", and the system will search for them exhaustively. The ability to concisely write test cases that cover vast areas of the search space is a major attraction of logic programming languages.
That's a good point, but the fundamental problem is that any sufficiently complex space takes way more than a practical amount of time to be completely traversed (easily reaching or surpassing age-of-the-universe magnitudes), so isn't that where fuzzy testing comes in? If you use something like Prolog, isn't it going to perform the search in an ordered manner, drastically reducing the chances that a hard-to-spot mistake is found? And at least in the general case, don't you have to restart the search from scratch every time logic changes?
- You write standard property-based tests with Hypothesis, which fit into a traditional unit-test workflow (run with pytest, or unittest, or whatever)
- then HypoFuzz runs indefinitely, using feedback-guided fuzzing to find bugs that random generation is too slow for
- restarting the search on each new commit (or daily, etc) begins by replaying every distinguishable input found earlier so you don't have to start from scratch. There is _some_ loss of context, but that's inherent in testing different code.
Plus other nice stuff like allocating more compute time to tests which are finding new behaviour faster, for maximum efficiency. The roadmap includes exploiting SAT solvers to find very very rare behaviour, git history to focus on recently changed code, statistical risk analysis ("what's the expected time to next bug"), and also a budget-mode ("my compute costs $. 08/hr, stop when you expect a bug to cost $100 or more to find"). It's a good time to be working on testing!
Generally more coverage is good, yes; but there can be an element of risk around "testing two inputs in isolation", to borrow terminology from the article.
You may have coverage for every line of function A and every line of function B, but that doesn't guarantee that results of the two - when combined dynamically at runtime - are going behave the way that is expected and intended.
I like your idea about tests that are continuously searching. In theory I also think it'd be possible to have tests that run on a codebase near-immediately as a developer types (even between commits); ideally exercising the data flow paths that are most directly affected by each syntax tree change first, and expanding out from there.
The problem with this is the requirement to build a completely controllable test fixture including OS syscalls. In practice that's very difficult.
How many people test their javascript in a sandbox that allows for arbitrary fault injection of calls into the browser API or DOM?
> If not, it will simply keep on searching, potentially for months, years, ... You need to be able to start the search for mistakes and keep it running for as long as you want.
You also need to ship, and in many startup environments you don't even have a deadline, it's just "as soon as possible".
Fuzzing is very effective at finding security issues. It does seem reasonable to try to design the security boundary of an application in a way amenable to fuzzing.
https://debbugs.gnu.org/cgi/bugreport.cgi?bug=496
https://debbugs.gnu.org/cgi/bugreport.cgi?bug=576
https://debbugs.gnu.org/cgi/bugreport.cgi?bug=726
Fuzzing is a useful approach and does reveal many issues with software. As I see it, the fact that fuzzing already tends to reveal mistakes in software is a good motivation to strive for methods that yield greater coverage based on systematic exhaustive search, so that also mistakes that occur in very rare situations are reliably detected.
Every time you want to say “manual” testing, ask yourself why you never say “manual programming.” Testing is a human, social process, just as programming is. Output checking can be automated, but so what? It’s not the essence of testing any more than running a compiler is the essence of programming.
Also verifying state of the app against the reference model after each testing step that modifies the state of the app also gets quickly out of hand. Even more so if you try to do it via public interfaces only.
And there's no guarantee your model is right.
1. The Ada feature of being able to specify the legal range of your type independent of its machine representation. Maybe it's stored in a system sized integer, but you want its legal values to be between 2 and 53. In Ada you can tell the type system this, and it will be enforced statically. You simply can't assign a value of 1, 0, -1, and code that could will be rejected as illegal.
2. The testing technique I saw in Haskell, where you specify the contract of the function to the testing tool, and it fuzzes inputs across the whole legal range of the type, trying to find corner case inputs that break it. This seems to be a more advanced version of informally specifying the contract by explicitly programming out every corner case yourself.
I suspect it would not be rare for a software project written this way to have more work hours spent on verification, than the development itself.
The use of shared code would be even more pervasive to the point whole library will likely be replaced rather than doing a single LOC change.
Hardware verification is very much like a pyramid. Verified cells are made of verified, and characterised to death semiconductor devices. Verified gates are made of verified cells. Verified logic components are made of verified logic gate designs. Verified libraries are built upon verified logic components. Verified blocks built upon verified libraries. And verified macroblocks built upon verified blocks.
P.S Did anyone else happen to run into this talk? I tried to find on youtube and could'nt find anything related.