denoland / std

The Deno Standard Library
https://jsr.io/@std
MIT License
3.11k stars 614 forks source link

add defer feature of Golang for testing #262

Closed axetroy closed 5 years ago

axetroy commented 5 years ago

defer in Golang is easy is useful especially in the test.

eg.

func TestSomething(t *testing.T) {
    filename := "test.go"
    CreateFile(filename)

    defer func() {
        RemoveFile(filename)
    }
}

do the same thing in Deno

test(async ({ defer }) => {
  const filename = 'test.go';
  await CreateFile(filename);
  defer(async () => {
    await RemoveFile(filename);
  });
});

I wrote a library before. https://github.com/axetroy/godefer

zekth commented 5 years ago

So defer is simply an helper for :

 await new Promise(res => {
    await RemoveFile(filename);
    res();
  });

Right?

axetroy commented 5 years ago

@zekth Not at all. @ry should know that.

here is the example

test(async ({ defer }) => {
  console.log("do first job");

  defer(async () => {
    console.log("1");
  });

  console.log("do second job");

  defer(async () => {
    console.log("2");
  });

  console.log("do third job");

  defer(async () => {
    console.log("3");
  });

  console.log("job done.");
});

// print out
// do first job
// do second job
// do third job
// job done.
// 3
// 2
// 1
zekth commented 5 years ago

I get it. Using the same defer behaviour LIFO after all the sync code has been executed.

j-f1 commented 5 years ago

Maybe call it cleanup()?

ry commented 5 years ago

I don’t like the idea because it needs to be called in a special context to work. (Unless I’m missing something?) It’s impossible to implement the general semantics that Go has.

axetroy commented 5 years ago

@ry In fact, we only need to create a context containing the defer function when execute each test function.

After the test function is executed (regardless of reject or resolve), then the defer queue is executed one by one.

This is achievable.

and it can be No side effects, No breaking API

Looking back, it can also add hooks like after

test(async ({ defer, after }) => {
  after(async () => {
    console.log('do the job after this test finish');
  });
});
ry commented 5 years ago

@axetroy But it only works in test, and the name suggests it can work elsewhere... If we could implement this generally, I'd be all for it - but I think that would be impossible without a compilation pass.

kitsonk commented 5 years ago

I am not sure about the semantics of Go, but Promises are already scheduled out of turn in JavaScript as a microtask. So all this would be is a function that would reschedule at the end of the current queue before the next macrotask. That should be implementable just in the runtime by creating a new Promise.

ry commented 5 years ago

In Go, the deferred function is executed before the function returns to caller, but after the body of the function is executed.

I haven't really thought thru how to implement this, but it seems not possible... If @axetroy has a way to do this in general, I'd be all for adding it. (It's super useful and a very nice feature.) But if it's restricted to test() callbacks, I think we should not do it.

kitsonk commented 5 years ago

I haven't really thought thru how to implement this, but it seems not possible...

One way, if there is a compelling use case, would be that the test function would always pass a function named deferred(cb: () => void | Promise<void>) which could then could schedule/drain the deferreds before the test function returns, as @axetroy indicates. Usage would be something like this:

test(function testSomething({ defer }) {
  defer(() => console.log("b"));
  console.log("a");
});

Anything throwing in there would be attributed to the test function. No magic, just "standard" JavaScript.

axetroy commented 5 years ago

After I finish this #261, I will do a PR.

axetroy commented 5 years ago

@ry I have implemented this feature.

try it out

save as defer.ts file

deno defer.ts
type DeferFunc = () => Promise<void>;
type Defer = (fn: DeferFunc) => void;

interface Context {
  defer: Defer;
}

type func<T> = (context: Context) => Promise<T>;

function deferify<T>(fn: func<T>) {
  return async function(): Promise<T> {
    const defers: DeferFunc[] = [];
    const context: Context = {
      defer(fn) {
        defers.push(fn);
        return;
      },
    };

    async function dequeue() {
      while (defers.length) {
        const deferFn = defers.pop();
        if (deferFn) {
          try {
            await deferFn();
          } catch {}
        }
      }
    }

    return fn(context)
      .then((r: any) => {
        return dequeue().then(() => r);
      })
      .catch((err: Error) => {
        return dequeue().then(() => Promise.reject(err));
      });
  };
}

deferify(async ({ defer }) => {
  console.log('do first job');

  defer(async () => {
    console.log('1');
  });

  console.log('do second job');

  defer(async () => {
    console.log('2');
  });

  console.log('do third job');

  defer(async () => {
    console.log('3');
  });

  console.log('job done.');
})();

// do first job
// do second job
// do third job
// job done.
// 3
// 2
// 1

If you think this is ok, can you reopen the issue?

ry commented 5 years ago

@axetroy Sorry - it's a bit too non-standard and boilerplate-y for me. Please submit it to https://github.com/denoland/registry