AxelG's avatar
Level 1

One specific test exceed memory usage

Hi,

I've got a problem that had me grinding my gears for several days. I wrote feature tests for a class, testing each routes with different cases, but here's the thing: every test works fine, except one case for one test.

Here are (some of) my tests:

    public function test_get(): void
    {
        $group = Group::factory()->create();

        // Succès
        $response = $this->getJson("/api/groups/$group->id");
        $response->assertStatus(200)
            ->assertJsonStructure(self::RESOURCE_STRUCTURE)
            ->assertJson(["id" => $group->id]);

        // Id du groupe invalide
        $response = $this->getJson("/api/groups/9999");
        $response->assertStatus(422);
    }

    public function test_update(): void
    {
        $group = Group::factory()->create();

        // Succès
        $response = $this->putJson("/api/groups/$group->id", ["label" => "Test2"]);
        $response->assertStatus(200)
            ->assertJsonStructure(self::RESOURCE_STRUCTURE)
            ->assertJson(["label" => "Test2"]);

        // Id du groupe invalide
        $response = $this->putJson("/api/groups/9999", ["label" => "Test2"]);
        $response->assertStatus(422);

        // Corps invalide
        $response = $this->putJson("/api/groups/$group->id", ["labl" => "Test2"]);
        $response->assertStatus(422);
    }

    public function test_delete(): void
    {
        $group = Group::factory()->create();
        $groupWithChildren = Group::factory()
            ->hasAttached(Group::factory(), ["order_number" => 1], "children")
            ->create();
        $test = Test::factory()->create();
        $groupWithResults = Group::factory()
            ->hasAttached($test, ["order_number" => 1])
            ->has(ResultGroup::factory()
                ->has(Result::factory()
                    ->for($test)))
            ->create();

        // Test simple
        $response = $this->deleteJson("/api/groups/$group->id");
        $response->assertStatus(204);
        $this->assertModelMissing($group);

        // Test groupe avec un parent
        $response = $this->deleteJson("/api/groups/{$groupWithChildren->children[0]->id}");
        $response->assertStatus(409);
        $this->assertModelExists($groupWithChildren->children[0]);

        // Test groupe avec des enfants
        $response = $this->deleteJson("/api/groups/$groupWithChildren->id");
        $response->assertStatus(204);
        $this->assertSoftDeleted($groupWithChildren);

        // Test groupe avec des résultats
        $response = $this->deleteJson("/api/groups/$groupWithResults->id");
        $response->assertStatus(204);
        $this->assertSoftDeleted($groupWithResults);
    }

Everything works, except the case "Test simple" ("Simple test" in french, which should be the most basic test case) for the test_delete() function. The thing is, I don't even get an error message! When I run the test suite with this case commented, it works, when I uncomment it, the terminal freeze for a very long time, then ends without any output (does not end at all).

I run the tests in a docker container by mounting the laravel application in the container. I think it's memory related because one time I managed to have an error saying "Allowed memory size of x bytes exceeded" by checking docker logs on the container. I can't even find that error again because now the command seem to run forever without crashing.

I made all kind of commenting to find the source of the problem, and this particular case is really what make everything goes wrong.

Here is the code that is called by the route if it can help:

GroupController

/**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy(int $id): Response
    {
        if (($group = Group::find($id)) === null) {
            return response("Le groupe n°$id n'existe pas", 422);
        } else {
            if ($group->delete()) {
                return response(null, 204);
            } else {
                return response("Le groupe est utilisé dans un ou plusieurs groupes", 409);
            }
        }
    }

Group

public function delete(): bool
    {
        if ($this->parents()->exists()) {
            return false;
        } else {
            parent::delete();
            if (!$this->resultGroups()->withTrashed()->exists() && !$this->children()->withTrashed()->exists()) {
                $this->tests()->detach();
                $this->forceDelete();
            }
            return true;
        }
    }

Can anyone tell me what is going wrong with this specific test case?

Is it a docker issue? If that's the case, why everything else works?

Is there some kind of mumbo jumbo laravel does when deleting that cause a loop or massive memory leak?

0 likes
1 reply
AxelG's avatar
AxelG
OP
Best Answer
Level 1

I found the problem. It was an infinite loop.

I wanted to override the delete() method of my model to either soft delete or hard delete it based on business logic. Found out that forceDelete() calls delete() before actually force deleting, so it was looping.

After reading the code for the forceDelete() method, I found the right way to do it:

public function delete(): bool
    {
        if ($this->groups()->exists()) {
            return false;
        } else {
            if (!$this->results()->withTrashed()->exists()) {
                $this->forceDeleting = true;
            }

            return parent::delete();
        }
    }

The forceDelete() method really just set the "forceDeleting" property to true before doing a delete, which disables the softDelete. With the above code, we basically emulate a force delete when our condition is true.

Hope it can help someone.

Please or to participate in this conversation.