Skip to content
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

add defer feature of Golang for testing #262

Closed
axetroy opened this issue Mar 11, 2019 · 13 comments
Closed

add defer feature of Golang for testing #262

axetroy opened this issue Mar 11, 2019 · 13 comments

Comments

@axetroy
Copy link
Contributor

axetroy commented Mar 11, 2019

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
Copy link
Contributor

zekth commented Mar 11, 2019

So defer is simply an helper for :

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

Right?

@axetroy
Copy link
Contributor Author

axetroy commented Mar 11, 2019

@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
Copy link
Contributor

zekth commented Mar 11, 2019

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

@j-f1
Copy link

j-f1 commented Mar 11, 2019

Maybe call it cleanup()?

@ry
Copy link
Member

ry commented Mar 11, 2019

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
Copy link
Contributor Author

axetroy commented Mar 11, 2019

@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
Copy link
Member

ry commented Mar 11, 2019

@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
Copy link
Contributor

kitsonk commented Mar 11, 2019

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
Copy link
Member

ry commented Mar 11, 2019

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.

@ry ry closed this as completed Mar 11, 2019
@kitsonk
Copy link
Contributor

kitsonk commented Mar 11, 2019

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
Copy link
Contributor Author

axetroy commented Mar 12, 2019

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

@axetroy
Copy link
Contributor Author

axetroy commented Mar 12, 2019

@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
Copy link
Member

ry commented Mar 12, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants