Jest test suite failed to run: 'Segmentation fault (core dumped)' or 'A jest worker process (pid=xxx) was terminated by another process'
Asked Answered
H

0

6

I'm creating an Express API and everything was going ok untill I started to have some problems with tests. I use Jest and Supertest to tests the endpoints and I don't know why I started to have this message when I run the tests.

 FAIL  src/modules/ingredients/tests/updateingredient.test.ts
  ● Test suite failed to run

    A jest worker process (pid=672269) was terminated by another process: signal=SIGSEGV, exitCode=null. Operating system logs may contain more information on why this occurred.

      at ChildProcessWorker._onExit (node_modules/jest-worker/build/workers/ChildProcessWorker.js:366:23)

(node:672290) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 open listeners added to [_0x323dee]. Use emitter.setMaxListeners() to increase limit
(Use `node --trace-warnings ...` to show where the warning was created)

I'm using Redis for caching and rate limiting and I know that it has to be realted to it but I don't know how to fix it. These errors only occur with endpoint tests. The most wierd thing is that these error only show if I run tests with VSCode open, when I run test from an external terminal and VSCode closed all the tests pass.

I have run test with --detectOpenHandles to see where is the problem and when I have VSCode open the outpout is:

Segmentation fault (core dumped) 

with VSCode closed is:

TCPWRAP

       5 | const redisDb = config.get<number>('redis_db');
       6 |
    >  7 | export const redis = new Redis({
         |                      ^
       8 |   port: 6379,
       9 |   host: '127.0.0.1',
      10 |   db: redisDb,

      at node_modules/ioredis/built/connectors/StandaloneConnector.js:44:21
      at StandaloneConnector.connect (node_modules/ioredis/built/connectors/StandaloneConnector.js:43:16)
      at node_modules/ioredis/built/Redis.js:121:64
      at EventEmitter.connect (node_modules/ioredis/built/Redis.js:103:25)
      at new Redis (node_modules/ioredis/built/Redis.js:77:18)
      at Object.<anonymous> (src/utils/redis.ts:7:22)
      at Object.<anonymous> (src/modules/allergens/tests/listAllergens.test.ts:5:1)

I have tried to use jest hooks to close Redis connections after all tests, before all tests, and after and before each tests in several ways I have found but it didn't work.

afterAll(async () => {
  await new Promise((resolve) => {
    redis.quit();
    redis.on('end', resolve);
  });
});

or

afterAll(async () => {
  await redis.quit();
});

I have reinstall VSCode, Node.js, used different version of Node, run npm rebuild, unistall jest extension of VSCode, deleted node_modules and npm install after but nothing works.

These are my files:

redis.ts

import Redis from 'ioredis';
import config from 'config';
import { Logger } from 'src/logger/logger';

const redisDb = config.get<number>('redis_db');

export const redis = new Redis({
  port: 6379,
  host: '127.0.0.1',
  db: redisDb,
});

redis.on('ready', () => Logger.info('redis ready'));
redis.on('error', (error) => {
  Logger.error(`error with redis connection: ${error}`);
  return redis.disconnect();
});

rateLimiter.ts

import { NextFunction, Request, Response } from 'express';
import { redis } from 'src/utils/redis';

export interface Limiter {
  windowSize: number;
  allowedRequests: number;
}
export const rateLimiter =
  ({ windowSize, allowedRequests }: Limiter) =>
  async (req: Request, res: Response, next: NextFunction) => {
    const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress) as string;

    const formatIp = (ipAddress: string) => {
      if (ipAddress.substring(0, 7) === '::ffff:') {
        return ipAddress.replace('::ffff:', '');
      }
      return ipAddress;
    };
    const formattedIp = formatIp(ip);

    const requests = await redis.incr(formattedIp);

    if (requests === 1) {
      await redis.expire(formattedIp, windowSize);
    } else {
      await redis.ttl(formattedIp);
    }

    if (requests > allowedRequests) {
      return res.status(429).send({ error: 'too many requests' });
    }
    return next();
  };

app.ts

import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { httpLogger } from './logger/httpLogger';
import { Logger } from './logger/logger';
import { errorHandler, isTrustedError } from './error/error-handler';
import { apiRouter } from './routes';

export const createApp = () => {
  const app = express();

  app.use(helmet());
  app.use(cors());
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(httpLogger);

  app.use('/api/v1', apiRouter);

  app.use(errorHandler);

  process.on('uncaughtException', async (error: Error) => {
    Logger.error(error);
    if (!isTrustedError(error)) {
      process.exit(1);
    }
  });
  process.on('unhandledRejection', (reason: string) => {
    Logger.error(reason);
    process.exit(1);
  });

  return app;
};

routes.ts

import { Request, Response, Router } from 'express';
import { rateLimiter } from './middlewares/rateLimiter';
import { allergensRouter } from './modules/allergens/allergens.router';
import { ingredientsRouter } from './modules/ingredients/ingredients.router';
import { recipesRouter } from './modules/recipes/recipes.routes';

export const apiRouter = Router();

apiRouter.get(
  '/health',

  (_req: Request, res: Response) => {
    const data = {
      uptime: process.uptime(),
      responseTime: process.hrtime(),
      message: 'ok',
      date: new Date(),
    };
    return res.status(200).send({ data });
  }
);
apiRouter.use(rateLimiter({ windowSize: 20, allowedRequests: 4 }));
apiRouter.use('/allergens', allergensRouter);
apiRouter.use('/ingredients', ingredientsRouter);
apiRouter.use('/recipes', recipesRouter);

ingredients.router.ts

import { Router } from 'express';
import { validate } from 'src/middlewares/validationRequest';
import {
  deleteAllIngredients,
  deleteIngredientById,
  findIngredientById,
  findIngredients,
  makeIngredient,
  patchIngredient,
} from './ingredients.controller';
import {
  createIngredientSchema,
  deleteIngredientSchema,
  deleteIngredientByIdSchema,
  getIngredientByIdSchema,
  getIngredientSchema,
  updateIngredientSchema,
} from './ingredients.schema';
import { cache } from 'src/middlewares/cache.middleware';

export const ingredientsRouter = Router();

ingredientsRouter.get('/', validate(getIngredientSchema), cache, findIngredients);
ingredientsRouter.get('/:id', validate(getIngredientByIdSchema), cache, findIngredientById);

ingredientsRouter.post('/', validate(createIngredientSchema), makeIngredient);

ingredientsRouter.delete('/', validate(deleteIngredientSchema), deleteAllIngredients);
ingredientsRouter.delete('/:id', validate(deleteIngredientByIdSchema), deleteIngredientById);

ingredientsRouter.patch('/:id', validate(updateIngredientSchema), patchIngredient);

ingredients.controller.ts

import { NextFunction, Request, Response } from 'express';
import { filterProperties } from 'src/utils/filterProperties';
import { isValidId } from 'src/utils/idValidation';
import { ApiError } from '../../error/ApiError';
import { redis } from 'src/utils/redis';
import {
  getIngredients,
  getIngredientById,
  getIngredientsByAllergen,
  createIngredient,
  updateIngredient,
  removeIngredientById,
  removeAllIngredients,
} from './ingredients.service';

export const findIngredients = async (req: Request, res: Response, next: NextFunction) => {
  const { allergenNames } = req.query;

  const properties = ['name', 'category', 'hasAllergens', 'allergens', 'allergenNames'];

  const filteredQuery = filterProperties(properties, req.query);

  if (allergenNames) {
    const parsedNames = Array.isArray(allergenNames) ? allergenNames : [allergenNames.toString()];

    try {
      const foundIngredients = await getIngredientsByAllergen(parsedNames);

      redis.setex(`ingredients_allergen_${parsedNames}`, 3600, JSON.stringify(foundIngredients));

      return res.status(200).send({ data: foundIngredients });
    } catch (error) {
      return next(error);
    }
  }
  try {
    const foundIngredients = await getIngredients(filteredQuery);

    Object.keys(filteredQuery).length > 0
      ? redis.setex(
          `ingredients_${Object.keys(filteredQuery)}`,
          3600,
          JSON.stringify(foundIngredients)
        )
      : redis.setex('ingredients', 3600, JSON.stringify(foundIngredients));

    return res.status(200).send({ data: foundIngredients });
  } catch (error) {
    return next(error);
  }
};
... more code

listIngredients.test.ts

import * as IngredientsService from '../ingredients.service';
import { IngredientInput } from '../ingredients.model';
import { createApp } from '../../../app';
import { redis } from 'src/utils/redis';

const app = createApp();

const ingredientInput: IngredientInput = {
  name: 'ingredient 1',
  category: 'eggs',
  hasAllergens: true,
  allergens: ['639eea5a049fc933bddebab3'],
  allergenNames: ['celery'],
};

const ingredientPayload = {
  _id: '639eea5a049fc933bddebab2',
  name: 'ingredient 1',
  category: 'eggs',
  hasAllergens: true,
  allergens: ['639eea5a049fc933bddebab3'],
  allergenNames: ['celery'],
};

const baseApiUrl = '/api/v1/ingredients';

const getIngredientsServiceMock = jest.spyOn(IngredientsService, 'getIngredients');

const getIngredientsByAllergenServiceMock = jest.spyOn(
  IngredientsService,
  'getIngredientsByAllergen'
);

const getIngredientByIdServiceMock = jest.spyOn(IngredientsService, 'getIngredientById');

beforeEach(() => {
  jest.clearAllMocks();
  jest.resetAllMocks();
  redis.flushdb();
});

afterAll(async () => {
  await redis.quit();
});

... tests start here
Hicks answered 26/2, 2023 at 18:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.