u413-284-si / webserv

This project is about setting up a http web server, providing a static website.
MIT License
0 stars 0 forks source link

22 implement connection class #37

Closed gwolf-011235 closed 3 weeks ago

gwolf-011235 commented 1 month ago

Summary after Update

Testing of Server functions was rather difficult since many clib functions. Abstracted away to make mocking possible.

  1. Modified Class Connection made into Struct Connection
  2. New Class EpollWrapper
  3. New Class SocketPolicy
  4. Redesign of server functions
  5. Redesign of handleConnection
  6. Added tests

1. Struct Connection

2. Class EpollWrapper

3. Class SocketPolicy

4. Redesign of server functions

5. Redesign of handleConnection

6. Tests

Summary before Update

  1. New Class Connection
  2. New Struct Socket
  3. Separation of epoll and socket opening: Server can now open one or more Sockets as Virtual Servers to listen on
  4. Handling of events either on VirtualServerSockets or Connections
  5. Handle Timeout
  6. Various

1. Connection

2. Socket

3. Separation of epoll and socket opening

4. Handling of events

5. Handle Timeout

6. Various

gwolf-011235 commented 1 month ago

Regarding the tests

Yes I can create a class which wraps the free functions and mock this wrapper class. This class would be passed to the server (similar to FileSystemPolicy()) so it can be injected as mock class also. Would you rather use the existing class (and maybe rename it to LibCWrapper() or something) or create a new one?

Also I would add this as an additional PR. Or should I expand on this one?

u413-284-si commented 1 month ago

Regarding the tests

Yes I can create a class which wraps the free functions and mock this wrapper class. This class would be passed to the server (similar to FileSystemPolicy()) so it can be injected as mock class also. Would you rather use the existing class (and maybe rename it to LibCWrapper() or something) or create a new one?

Also I would add this as an additional PR. Or should I expand on this one?

I would create a new one specific to these functions and expand this PR as it is part of the testing procedure imo.

gwolf-011235 commented 1 month ago

I've got a problem with testing!

In principle it is said that you should not test private functions because you should test the interface and not the implementation. This improves encapsulation / abstraction and maintains flexibility in the implementation. Sounds good - in principle.

In this context, I also read about "iceberg" classes. These are classes that combine (too) much functionality and then only have one or two public functions ("tip of the iceberg"). This should be avoided, since it violates the Single Responsibility Principle and a bunch of other principles. It also makes testing complicated, leading to poorer coverage. The advice is to create smaller, focused classes with clear responsibilities and well-defined public interfaces. I tried to go with this approach with the ResponseBuilder: of course you suddenly have a lot of classes.

Or you can ignore the recommendation and penetrate the class and test the private functions as a friend or with preprocessor fun (private = public).

There is also the approach of not putting it in any class, but use free functions that can be easily tested in isolation. This is often found in data-oriented programming. But also object oriented style points into that direction: for example Scott Meyers says non-member functions improve encapsulation: https://www.aristeia.com/Papers/CUJ_Feb_2000.pdf (written way back in 2000!) I chose this approach for #22. The disadvantage here is that the function params explode quickly.

There are currently two dependencies to mock in the server:

The server itself is set up in such a way that it initializes the virtual server(s) in the constructor and then waits for connections in an endless loop. I find testing difficult because I can't check the status of the server from the outside. For example if I init a dummy server, I would need to access the m_vitualServer map inside the server, to check if everything set up correctly.

With the free functions it's a bit silly because you have to enter so many params. But it is extremely easy to test, since you can check each function in isolation.

Any different ideas? I have to admit I like the free function approach, but the param list can be daunting.

QCHR1581 commented 1 month ago

I've got a problem with testing!

In principle it is said that you should not test private functions because you should test the interface and not the implementation. This improves encapsulation / abstraction and maintains flexibility in the implementation. Sounds good - in principle.

In this context, I also read about "iceberg" classes. These are classes that combine (too) much functionality and then only have one or two public functions ("tip of the iceberg"). This should be avoided, since it violates the Single Responsibility Principle and a bunch of other principles. It also makes testing complicated, leading to poorer coverage. The advice is to create smaller, focused classes with clear responsibilities and well-defined public interfaces. I tried to go with this approach with the ResponseBuilder: of course you suddenly have a lot of classes.

Or you can ignore the recommendation and penetrate the class and test the private functions as a friend or with preprocessor fun (private = public).

There is also the approach of not putting it in any class, but use free functions that can be easily tested in isolation. This is often found in data-oriented programming. But also object oriented style points into that direction: for example Scott Meyers says non-member functions improve encapsulation: https://www.aristeia.com/Papers/CUJ_Feb_2000.pdf (written way back in 2000!) I chose this approach for #22. The disadvantage here is that the function params explode quickly.

There are currently two dependencies to mock in the server:

  • EpollWrapper (simulates an epoll instance) and
  • SocketPolicy (has functions such as socket() and accept() ). Both are injected so that the mock variant can be used if required.

The server itself is set up in such a way that it initializes the virtual server(s) in the constructor and then waits for connections in an endless loop. I find testing difficult because I can't check the status of the server from the outside. For example if I init a dummy server, I would need to access the m_vitualServer map inside the server, to check if everything set up correctly.

With the free functions it's a bit silly because you have to enter so many params. But it is extremely easy to test, since you can check each function in isolation.

Any different ideas? I have to admit I like the free function approach, but the param list can be daunting.

I am not quite sure if I understand that correctly, but why can't you access the m_virtualServers map for testing purposes?

gwolf-011235 commented 1 month ago

I would need to write a getter for the m_virtualServers map, since it's private.

The sole purpose (right now) of this getter would be for testing.

I could also have getters/setters to reduce the free function params, and pass a ref to server (I think this would be the way of the article)

Right now it does work. The question is: is there a better alternative?

QCHR1581 commented 1 month ago

I would need to write a getter for the m_virtualServers map, since it's private.

The sole purpose (right now) of this getter would be for testing.

I could also have getters/setters to reduce the free function params, and pass a ref to server (I think this would be the way of the article)

Right now it does work. The question is: is there a better alternative?

I didn't mention it before, but the article was really an interesting read. I never thought about the use of non-friend non-member functions instead of member functions for the purpose of increasing the encapsulation. Thanks for for the link!

In terms of your need to check the server status, I am not absolutely sure what the best approach is. GoogleTest provides a FRIEND_TEST macro for enabling the test to access private class members. Perhaps this is an alternative solution?

At the end of the project we have to remove all test we have created when we submit the project. Therefore you could use this test and then deleted when it is not needed anymore.

gwolf-011235 commented 1 month ago

I think friend testing has the problem that you have to add (and in our case remove) friends to the class. I think this is too intrusive especially since we have full control over our code.

Right now I lean towards adding getters and other interface changes to make it more testable.

Then I'll still have non member functions which get passed a reference to server. That way the amount of things to pass to the functions is reduced.

I'm not sure if it's good design then, but at least it makes it testable (which kinda also in itself is good design)

u413-284-si commented 1 month ago

To weigh in this discussion I can share my current experience at work:

We are creating Unit tests for existing classes to increase the test coverage of our code base. The approach to many of those classes is to add additional methods (getters/setters/helper functions) as well as Constructors which serve only for testing - and are marked as such. I cannot say if that's good practice, but it is at least not uncommon.

Therefore I am as well in favour of extending the interface to enable better testing. External functions with lots of parameters should best be kept to a minimum.

gwolf-011235 commented 1 month ago

Good update for making the server easier testable, using struct instead of class for Connection and also abstracting functions!

One thing I want to emphasize about this PR is that it divides the processing of a connection into different phases (ReceiveRequest, ReceiveBody etc)

I think this way we can serve multiple clients better since we don't handle everything from one connection and then work on the next connection. We do a little bit here and a little bit there. This gives us flexibility:

We could also use a process per connection, since fork() is allowed. But I think that makes everything harder to deal with. We will have to use fork for CGI anyway.