-
Notifications
You must be signed in to change notification settings - Fork 5
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
Consider using ts-semantic-testing #2
Comments
😅 Honestly, I love this -- it's heart-warming this experiment to do a type lib is already spawning tooling specific to this niche. I'll try it out. |
Have fun, feel free to request things or contribute or whatever if you'd like. I've actually been thinking I may completely change around the API though, to something roughly like this. Basically break it away from transforming the full test files and tests, and just provide a single block that is transformed and makes any semantic errors available for assertions, in a completely test framework independent way. it("uses the new rough API idea", () => {
tsst(() => {
// Semantic errors within here are returned as strings
// Only the contents of tsst are transformed
type A = B;
}).expect.toNotFindType("B");
}); So feel free to play around with it, and use it if it seems useful in its current state, but beware this is all very prototype right now and will change drastically. |
Oh, and while it's designed for this niche, I do wonder if semantic tests may sometimes be useful in more general contexts. The organisation that comes with a testing framework may be a nicer experience than just looking out for compile errors in some situations. |
Features looked for to test types (also see sanctuary-js/sanctuary#254 (comment)):
Seems to me that for this use-case you already had the right feature-set in mind.
I'd be curious about its potential as well. Then again, normally people make tests to prevent regressions from sneaking in, whereas it's harder for compilation errors to sneak in without noticing upon compilation. In that sense, use-cases elsewhere might be more limited. On it("works with the number 0", () => {
type A = ZeroOneToBoolean<0>;
}); What I had now in my attempt to test without names: Imagined way the syntax could collapse: it("is a number", the<number, 123>); ... Which would fail since generics for functions could only be provided upon calling them. They can be provided for types, but we want an expression-level function here. I guess one could be like let x = null!; // short expression-level `never`, can be cast to anything
type TheF<T, V extends T> = () => V; // type-testing function type to lift to expression level
// options to invoke without calling function:
x as TheF<number, 123>;
<TheF<number, 123>>x;
// imagined test:
it("is a number", <TheF<
number,
123
>>x); More of a thought experiment than pretty syntax. Also fails to yield a function expression on type error though which I suppose screws it up. Do you have an example project you've actually already used it on by the way? |
Put your example snippet in some sub-folder 'repository' and have a script calling it on that folder? Maybe have your CLI propagate the status code of the testing thing so you'd know where the testing thing reported errors? You could e.g. have one test project like that that should pass, another that should result in some error so as to check whether it'd correctly report success/failure based on your transformed types, I dunno. |
Hm. If the I suppose if you could put your own example project for this up there it might serve simultaneously as both documentation on usage as well as a basis for testing 😅, right now running |
I'll work on getting a proper example out soon sure, that sounds good. And currently It's literally just this: const projectPath = project || "./";
const configPath = ts.findConfigFile(projectPath, ts.sys.fileExists);
const basePath = path.resolve(path.dirname(configPath)); /*?*/
const configReadResult = ts.readConfigFile(configPath, ts.sys.readFile);
if (configReadResult.error) throw new Error("Error reading tsconfig.json");
const config = ts.parseJsonConfigFileContent(configReadResult.config, ts.sys, basePath);
const program = ts.createProgram(config.fileNames, config.options);
const transformer = makeTransformer(program);
program.getSourceFiles()
.filter(file => minimatch(file.fileName, glob))
.forEach(file => program.emit(file, undefined, undefined, undefined, {
before: [transformer]
})); |
Thanks! That's illuminating, let me try for a sec then. |
This stuff is great, love to see progress in this space. |
Oh, I installed a local version figuring globals are an anti-pattern with version control, then erroneously tried running Edit: not outputting code, even with |
Oops, sorry. Yeah, I'm working on the example now, it's a little tricky to work out how to nicely integrate with an existing test framework because you have to do the transformations before the tests... I think the version I put up will be working but a mess, will have to find/build a better way at some point. Just to be clear about running the build, "scripts": {
"test": "tsst **/*.test.ts"
} |
I've clearly explained that awfully, sorry. I'm prototyping/iterating significantly faster than I'm documenting. |
Sorry, I ended up mostly having to deal with some other boring stuff today, didn't get as much time to work on this as I would have liked. https://github.com/TheOtherSamP/tsst-example-project is now a thing, I think it works. I've been having a slight issue installing it, I've been intermittently getting a weird
error when |
Oh, and I completely changed the syntax around, surprise! |
Hm.
Edit: incidentally |
Huh, I'm on Windows and don't have easy quick access to a Ubuntu environment for testing right now. I should probably mention that I'm more of a .Net dev historically, I may be making silly mistakes here, I'm learning as I go, sorry.
Works for me.
Strange. I'll clear everything out and see if I can work out what's going on there. I don't think I've done anything particularly weird with NPM here, so not sure what's up with that.
On it now, sorry. I should have had that set already, my bad.
I'll throw some logging in there quickly. I'm not immediately sure what's going on with that error either. Could that be a Unix thing with path handling somewhere? Apart from the NPM thing, just copying down the folder from github and running locally just works for me. Aaand this already became a support job. 😂 |
Nah it's good. I only switched over half a year ago when I realized my most-wanted Windows 10 had become better BashOnWindows support. We're all learning anyway. I'll try to figure out what the heck is going on with that glob (edited previous post a bit but still on it). Other two should be fixed with LF and using local install. |
Sorry, seems quotes matter for me, my bad.
So file name |
My bad, swapping |
Hmm, but I think that should have worked. I just threw the standard minimatch in there and assumed it would just work, but that does seem to be an issue, maybe I need to mess with options for it or something. I'm actually seeing the same thing here when I set that to Nothing's ever simple. 😂 |
Transpilation now works fine. Running |
No, that's actually just a buggy regex I wrote. Your error is write, the confusing thing is that it works for me. |
Thanks, yeah, just saw it. Made a PR, probably at the same time as you fixing it haha. |
Uh, actually, that 'fix' is wrong. Would |
I've been awake for about 30 hours and drank too many coffees, try not to judge me too harshly for how much of a mess I just made of the commit history. No, blame me, that's fair. 😂 |
Actually I'm going to disappear and sleep now before I accidentally destroy this whole project. |
Get some rest, you deserve it 😃, I'll try to see what I can do to incorporate this over here! Heck, I'm pretty sure @gcanti should be interested too! |
Have fun, thanks for all the help. If I don't spend the whole day battling the weird NPM issue I think I'm going to try to get a basic expectation/assertion system in it tomorrow. And fix the millions of bugs I've probably sleepily put into it today. |
Yeah, I'll try a bit as well, expect another PR or two. |
Note that the setup of I tried to chop the Moreover though, as the regular TS compilation process is skipped, meaning it will transpile the import statements, which will then no longer have anything to import from. Being able to import Also tried for a bit to see where I could take this:
... to something terser too (now I can actually test), but not much luck without touching transformers at least. |
As a target, I'd also like to be able to compare an object instance against an interface to assert that return values really do conform to their types, but I think that's probably fairly far off still for now. |
By "fairly far off", I may well mean impossible. Things like fully checking the return types of functions will likely never be possible. That idea is still in the early stages of wouldn't-it-be-nice-if, don't expect it to become reality. |
Although, if you fancy putting in a PR that solves the halting problem, I'll get right on that. |
Return types feels hard, yeah -- if it's based on generics things quickly get hard to track in a generic way. I mean, try that for Ramda's The halting issue... so so far I got to have it crash only a single file rather than the whole script, which helped. A better solution... probably involves parsing the whole project without type checking, then separately checking the types in those Hopefully something like this to further quarantine the halt to the test level, rather than the file level... It sounds like that means there would no longer be file-wide type checks as before though, which... sounds... kind of breaking / different. |
Oh, I'd completely forgotten about that halting issue, hah. I was actually just trying to make a bad joke about the impossibility of determining the return type of an arbitrary function because of the halting-problem in the comp.sci sense, but I do need to look into that TS problem too. Yeah, we definitely need to type-check the whole thing, otherwise the tests are pretty much completely pointless - they'd only be able to check types defined within themselves. I think ultimately, other than crashing gracefully, dealing with that issue is out of scope. I see that as a TS crash that's on them to fix. Best case I might be able to detect it and suggest what's happened in the error message. In terms of the comparing against interfaces idea I suggested, my current thinking is that it's probably theoretically possible with most things apart from functions, because we can't test what happens for every possible parameter or state. The best idea I have right now is somehow convert arbitrary objects into a form the compiler understands (maybe impossible) and pass them in and do checking by hacks similar to how I'm currently doing |
I still haven't looked into it, so this may not make sense, but I'd assume the best way for TS to solve the problem would be to fail as a type error if it detects that it can't resolve the type. It would be a weird (but hopefully rare) error for developers, but it's probably the best option unless the problem can be eliminated. |
Yeah. I thought they already had some similar errors for types that seemed 'too' recursive. It definitely isn't practical to resolve from our end here.
So bridging run-time (JS) and compile-time (type) testing huh? |
By the way, short-term gains might be to just npm publish a current (pre-refactor) version, optionally git install proof so you wouldn't need to npm publish every single commit to make them consumable. :) |
My intuition is that it's doable for the simple cases, but impossible in the general case. Which arguably makes it useless.
Doing this for a JSON-serialisable object is easy. Doing this for any object is hard. Anything that can't be represented by an object literal, I think we're going to hit a roadblock with. If we can't reliably assess any object that we feed into the test then the test is pretty much useless. We may be able to make it work for any object, but only for a subset of all possible interfaces. At that point it's still a hassle, of limited use, and probably not worth it. |
Yeah, I'm not actually even at the machine right now so I can't do this instantly, but I'll set myself the goal of getting a version up within 24 hours. Don't be surprised if I fail that goal though. |
Although I'm not 100% sure I can't take an arbitrary object and reconstruct a TS compiler |
Chances of the TS compiler actually exposing everything I'd need to do that at the moment are very low, too. |
Isn't TS's raison d'être to gradually get to this point? Might be worth it to try and see how far off it ends up.
I suppose you did have the step of parsing a string into a node, if for a whole file? |
Yes, this is definitely possible, I'm doing it now. Admittedly my current way of doing it is not ideal for units smaller than files (I create a Simple example of a hard object: const hardObject = {};
hardObject.self = hardObject; If we just have variable *thought happens* Oh, okay, actually maybe there is. For some reason I was thinking in terms of converting the object into an object literal in string form (so basically JSON but with basic function signatures) and giving that to the compiler, but actually it may well be possible to take a runtime object and produce a typescript code for an interface of that object. So the interface $$tsstGeneratedInterface1OrWhatever {
self: $$tsstGeneratedInterface1OrWhatever;
} Maybe this is what you were thinking all along. Okay, so it now seems more achievable. There are still problems though, particularly around generics.
// The best interface we could possibly generate under ideal conditions for a Map<K, V> without
// special code that knows to check the keys and values properties and set signatures accordingly.
// Comparing this against Map<string, number> is useless, it can't check those generics
interface $$tsstGeneratedInterface2OrWhatever {
// More realistically this would be `get(...args: any[]): any;`
// Ideally we could sometimes get some info from function decompilation
get(key: any): any;
set(key: any, value: any): any;
...
} So, we can do a bit more than I originally thought, but I'm still pretty sure that the lack of signatures means a lot of real world scenarios will fail. If it can't be relied upon to work for everything, it's effectively useless. |
Fair enough! Admittedly recursive objects hadn't really occurred to me. Admittedly in those cases things may get tougher. I'd argue even before reaching perfection this may still be useful though. Same as TS itself!
I'd suggested generic inference in #14078 before, which @masaeedu incorporated into a more pronounced proposal at #17428. So hopefully that'll improve, though perhaps the bigger problem for functions isn't even so much a TS one, but rather JS level function comparison. I imagine that's identity-based, which fails for testing purposes I suppose. In that sense, I suppose realistically functions will likely end up having to be tested based on their results in practice, i.e. testing not the function directly, but a few applied versions so testing becomes doable again both for JS as well as for type inference purposes.
So in these interface-value matching checks, the interface can be regarded as a LHS, the value as a RHS. That means in order to get the most realistic check, we'd prefer for the inferred type on the value to be as granular as possible, i.e. no widening if it can be avoided. This means we wouldn't infer
|
Absolutely. The restrictions aren't in TS, they're in JS. The whole point of TS is to add extra type information that JS doesn't have, and we can't recover all of that from JS objects. That's unsolvable unless JS add type information.
I don't know, I think it might have to be. I don't personally see much value in a test with unreliable results. Let's say we have the following interface somewhere in our source: interface Foo {
bar: string | number;
} Okay, we can test that fine. const foo = someFunctionThatShouldReturnFoo();
// Not what the actual syntax would be
expect( tsst(foo).is<Foo>() ).toBeTrue(); That works, we should be able to make Well now the test still passes because |
fwiw, from So there's a limitation there, but it's not even imposed by your additions. And heck, no-one's claimed Jest to be useless over this limitation yet. So essentially functions would instead be tested indirectly I guess. That works. In fact, for ES6 Set/Map the exact same limitations apply -- the comparison doesn't even work at Jest's run-time as JS would apply identity-based equality checks. Perhaps when we get something we can't compare (function/Set/Map) we could give some warning notifying the user part of the test is not meaningful (for either run-time or compile-time checks!). Functions could still be tested in separate tests applying them. Admittedly sucks for |
Hmm... Throwing up warnings or failing when the checked interface doesn't comply with our restrictions is an option, but I think it would quickly get too abrasive for me to ever use. The interface/object comparison check idea is really just a convenience for testing the same things we currently check manually property-by-property using standard testing procedures. If we had to worry about tests breaking and having to be rewritten if we choose to add one property to an interface that would completely negate the convenience for me. |
Hm. Perhaps the reason I didn't feel as reluctant is I've personally largely avoided the ES6 structures (and say Immutable.js) in favor of plain structures, mostly since those seemed to work nicely with the functional paradigm offered by Ramda that static type checking kind of depends on. Following that paradigm (separating logic and data), I wouldn't normally mix functions into such object structures either. In that sense, my own style would likely not have suffered much from these limitations. I think the good part here is that JS limitations already means many of these cases would already have stayed out of Jest tests, but yeah, not Set/Map I guess. |
That's fair. It's worth noting that it's more than just The basic problem is still one I'm interested in solving, but I'm not convinced this is the solution. |
On second thought... the checks might be in place already. |
If I may bounce off an idea since you have a bit more experience here, could I say parse a typings lib and get its typings for different exports? Use-case: use function info to allow the user to give a few params, and filter down the list of functions to those usable for those inputs. I figured some FP libs like Ramda primarily face adoption issues from the learning curve of their overwhelming number of functions, so I figured it'd be cool if I could help users a bit find the functions they need. |
Sorry, yet again something came up and I haven't got this done. I think I may have the time over the next couple of days, but I've learned not to make that commitment.
That's an interesting idea, and I think the answer is yes, partially. Pulling out all of the exports shouldn't be hard. You can get the signatures out of a function in a few ways, including from the type and from the declaration nodes. So getting a list of function names and their signatures shouldn't be too hard. The place where it gets a little tricky is filtering those signatures. What format were you expecting the user to give params? Actual values? Strings representing types? Would this be running in an environment where you could use the compiler itself in your filtering interface? |
Thanks! That seems exactly what I was looking for. :)
This part I'd considered already. I fear this becomes just brute-forcing the potential positions of known input parameters (or rather, their types) to check if any work. :)
I'd imagine the TS compiler would be loaded into the browser here, yeah. I looked at JSDoc data as an alternative way to check function signatures (over TS), but those seem to lack even basic meta like function names.
Values. The idea would be to just execute the legitimate options so the user could just see the actual results by function (+ param order, if multiple match). |
Well good luck, and be warned that matching the signatures may prove quite hard, I'm not sure how I'd do that. The language service also provides some methods for autocompletion that may be of interest here. I don't think that would give you the info you want directly, but the source of methods like |
Tried for a bit, but I did still run into bumps, yeah. |
Right, yeah, sorry. I've had about four other high priority projects going on and couldn't particularly justify throwing time into this fun side thing, but you're waiting on it and I said I would so I'll get something out today/tomorrow. I'm actually just heading to bed, but I'll work on this first thing and get out a basic MVP. |
Eh, if I needed something changed I should just try and add it myself, it's just the npm repo isn't mine to publish to. :) |
Hey, so just an update because I'm late again. I went to do that and found there was a significant bug still in it that I'd forgotten I'd only got half way through fixing, so it wasn't actually functioning at all. It's a little more work to get a releasable version than I'd thought, but I am actively working on it when I can. I'll tentatively say a day or two. |
Would it be doable to just git checkout the version before the refactor and publish that? I hope that should help alleviate pressure on your end. |
I've just published an early version prototype of an idea to help with writing tests for these kinds of projects, ts-semantic-testing. It's still very early days and potentially buggy and all subject to change, but I thought you might find it interesting to have a look at.
The text was updated successfully, but these errors were encountered: