davmac314 / dinit

Service monitoring / "init" system
Apache License 2.0
593 stars 50 forks source link

Replace use of iostream-based classes #240

Open davmac314 opened 1 year ago

davmac314 commented 1 year ago

Unfortunately, C++ iostream facilities suck. They are heavyweight due to supporting different character encodings. They make it impossible to get useful error messages in a standardised way (the language spec allows for it, but the implementations apparently don't bother and map all error conditions to the same useless message). std::ios::failure extends std::system_error but doesn't use errno as the error code.

We should replace them (including use of cout/cerr) with a wrapper for POSIX file I/O.

mobin-2008 commented 1 year ago

Do you mean we need to use printf() and friends in all cases or replacing something like ifstream with pure C functions like open() or even both? for example: https://github.com/davmac314/dinit/blob/a6cff08795ffaea003c1e893c4b3ec96d81a6ae9/src/dinit-env.cc#L20-L26 to this?

// Read and set environment variables from a file. May throw std::bad_alloc, std::system_error.
void read_env_file(const char *env_file_path, bool log_warnings, environment &env)
{
    int env_file = open(env_file_path, O_RDONLY);
    if (env_file == -1) {
        return
    }
    //env_file.exceptions(std::ios::badbit);
davmac314 commented 1 year ago

No, not necessarily, I meant replace use of iostream (including fstream etc) with a similar wrapper class which uses open and write/read internally. It is still good to use a class rather than use POSIX functions directly as it allows using the RAII idiom.

mobin-2008 commented 1 year ago

I worked on it a bit and I really enjoy, @davmac314 Please assign this issue to me if you wish. for example I have a very basic implantation for replacing cout and cerr with very better code generation:

#ifndef DINIT_IOSTREAM_H
#define DINIT_IOSTREAM_H

#include <baseproc-sys.h>
#include <cstring>
#include <cmath>

// ToDo: Desc

namespace dio {

class ostream
{
    public:
        ostream operator<<(const char *msg)
        {
            bp_sys::write(1, msg, strlen(msg));
            // ToDo: Error handling
            return *this;
        }
        ostream operator<<(std::string msg)
        {
            bp_sys::write(1, msg.c_str(), msg.size());
            return *this;
        }
        ostream operator<<(int num)
        {
            char tmp[(int)((ceil(log10(num))+1)*sizeof(char))];
            sprintf(tmp, "%d", num);
            write(1, tmp, strlen(tmp));
            return *this;
        }
};

ostream cout;
// ToDo: cerr should print to fd 2 (stderr)
ostream cerr;
}

#endif

and dinitcheck compiled and ran with no issues. We need buffer for stdio?

q66 commented 1 year ago

the shift operator overloads are a bunch of gross 90s cargo cult, so while replacing them i'd suggest actually making up reasonable interfaces

low level file descriptor funcs are too primitive for reasonable i/o though, so the implementation may get relatively complex

mobin-2008 commented 1 year ago

the shift operator overloads are a bunch of gross 90s cargo cult, so while replacing them i'd suggest actually making up reasonable interfaces

With something like fmt?

davmac314 commented 1 year ago

@mobin-2008 -

Input is the more important issue, and more difficult.

For output, fmt is probably overkill. Look at log (in dinit-log.cc) for something closer to what I was thinking.

Yes, there should be buffering. Particularly for input but ideally for output also.

There are a lot of subtle issues involved in getting this right, so please think carefully, and consider the requirements of the project, existing style and practice, etc. Avoid memory allocation where practical. Throw exceptions appropriately. Provide quality code documentation (comments).

davmac314 commented 1 year ago

Note that your sample implementation already contains things I would outright reject:

mobin-2008 commented 11 months ago

After a while, I'm so happy to announce that iostream and fstream are mostly ready and just needs to be clean-up ed in some sections.

Feel free to take a look at it: https://github.com/mobin-2008/dinit/blob/iostream_replace/src/includes/dinit-iostream.h https://github.com/mobin-2008/dinit/blob/iostream_replace/src/dinit-iostream.cc https://github.com/mobin-2008/dinit/blob/iostream_replace/src/includes/dinit-fstream.h https://github.com/mobin-2008/dinit/blob/iostream_replace/src/dinit-fstream.cc

Best regards

davmac314 commented 11 months ago

@mobin-2008 I had a quick look. It's an ok start but I do have lots of concerns and comments.

Some stylistic things:

Design-related:

There's probably more but that is at already a lot of things to think about.

It's all very clearly based on the std::iostream design and I think it'd be good to really think about how you see these classes being used and whether you can improve on the std::iostream interface. Remember the whole point is to replace it with something better, not just to re-implement the exact same thing. Consider things like:

Note that these are genuine questions, I'm not saying the answer is "yes" or "no" in any case, but they should be considered. I'm not sure if you've thought about them. If you have, that's fine, but perhaps you should document your rationale as part of the documentation.

Finally: lots of documentation (comments) is needed! Right now so many things are unclear. It shouldn't be necessary to look at the implementation to understand how it should be used.

mobin-2008 commented 11 months ago

Most of style things are fixed. Also I started working on documention and ostream is now completely documented. bits are real "bits" now and can be set simultaneously.

ostream::_write always calls writev and tries to write the buffer contents, which means that output effectively isn't buffered in a lot of cases.

I disagree. put() (formerly _write()) only write on these situations:

/*
 * All calls to write() will be buffered but not necessarily written to file descriptor except
 * on these situations:
 *
 * 1. Message has \n (newline)
 * 2. "flush()" (or its exception-free variant: flush_nothrow()) be called
 * 3. "flush" object be passed to write() (equals to calling flush())
 * 4. "endl" object be passed to write() (equals to writing \n and calling flush())
 * 5. Object be destructed (which make a call to flush_nothrow())
 * 6. Buffer be full
 */

Which is OK in my opinion.

Could/should (some, or all) error conditions be returned as failure code (or even exceptions) from individual functions, rather than being kept as state in the stream object?

Hmm, I'm not so happy with returning error conditions in function because it makes into mess. I think a class should maintain its status.

@davmac314 Question: It's safe to assume that EINTR, EWOULDBLOCK and EAGAIN will be changed in one point? my flush() tries up to 10 times about those errnos. I afraid to they get stuck and the whole program get stuck on infinite while loop design.

Current ostream class documention (long description):

Details

```c++ /* ostream * =-=-=-= * * This class is designed to provide basic functionality around the output. It maintains * a cpbuffer (with a fixed size which can be changed in compile-time) and stores file descriptor number * in its private space. it supports empty constructor and constructor with file descriptor * as parameter: * * dio::ostream output_a; // Simple construction of ostream * dio::ostream output_b(3); // Construction of ostream with file descriptor No. * * Also setting (or changing) file descriptor after construction is available by setfd() * function. * * copy/move constructors/assignments and destructor * ------------------------------------------------- * * ostream copy constructor is prohibited (copy constructor is deleted). * * ostream move constructor is available. All variable contents except of the buffer * (and its contents), will be moved. Also buffer of moved object will be unavailable * (by reset() call). * Class definiton has full list of what will be moved from given object. * * ostream copy assignment is same as copy constructor. * ostream move assignment is same as move constructor. * * ostream desctructor will call flush_nothrow() on desctructing. * * Public member: write() * ----------------------- * * ostream provides write() functions, which support many types of data for writing into fd(s). * See class definition for full list of types which supported by write() functions. * It's possible to combine different types in a single call with "," charactor also: * * output_a.write("This is a example message.\n"); * output_b.write("2 + 2 equals to: ", 2 + 2, endl); * * All calls to write() will be buffered but not necessarily written to file descriptor except * on these situations: * * 1. Message has \n (newline) * 2. "flush()" (or its exception-free variant: flush_nothrow()) be called * 3. "flush" object be passed to write() (equals to calling flush()) * 4. "endl" object be passed to write() (equals to writing \n and calling flush()) * 5. Object be destructed (which make a call to flush_nothrow()) * 6. Buffer be full * * write() functions guarantee to return number of actually written charactors not number * of buffered charactors. Also they can return -1 on failure cases (currently badbit). * * Public member: flush() * ---------------------- * * ostream provides flush() function to write all contents of buffer. Passing flush object * to write() functions equals to call flush() on the stream. This function will return * number all written charactors or -1 on failure cases (currently badbit) * * Error handling and Stream states * -------------------------------- * * ostream states are: goodbit and badbit. * goodbit is true by default and means "Everything seems to be normal". * badbit is 0 by default and means there is something wrong from POSIX I/O calls. It's get * set to POSIX errno in this case. * * For checking current stream states these functions are available: * rdstate(): Returns diostates::goodbit or diostates::badbit based on current state bits. * good(): Returns 1 or 0 based on goodbit is set or not. * bad(): Returns 0 or POSIX errno based on badbit value. * * For setting current stream state(s) these functions are available: * clear(): Sets badbit to 0 and goodbit to true. * setstate(): Set requested bit to requested value. * * NOTE: goodbit cannot be set directly to false by hand! goodbit has conflict with other bits * and setting other bits (e.g. badbit) true will make goodbit false. * * Exception handling * ------------------ * * exceptions() can be used to set when should a exception be thrown. For ostream it accepts * diostates::badbit. Also exceptions(0) will disable all cases which can throw exceptions. * * See description for each function to know what exceptions can be thrown by functions. * * All of exception-throwing functions have exception-free variants which marked by * "_nothrow" suffix. Those functions guarantee not to throw any exceptions regardless of * the exceptions() setting. */ ```

davmac314 commented 11 months ago

I disagree. put() (formerly _write()) only write on these situations:

Ok, I see I misread it. But:

  1. Message has \n (newline)

Why flush just because of newline? And:

  1. "endl" object be passed to write() (equals to writing \n and calling flush())

Why does it call flush() if writing '\n' already does flush?

Question: It's safe to assume that EINTR, EWOULDBLOCK and EAGAIN will be changed in one point? my flush() tries up to 10 times about those errnos. I afraid to they get stuck and the whole program get stuck on infinite while loop design.

The stream library doesn't have sufficient context to make a decision about how to handle these errors, so it should report them to the application. It should not try the operation again, it's for the application to decide whether that is the right thing to do.

It might have made sense to have a mode which ignores and retries on EINTR, but I wouldn't bother with that now. For EWOULDBLOCK/EAGAIN just repeating the operation is completely wrong, you will end up repeating the same operation (with the same error) in a tight loop and send CPU usage through the roof. These conditions can only be cleared by something external (eg another process on the other end of the pipe) so you can't ever rely on that happening in any timeframe at all.

davmac314 commented 11 months ago

@mobin-2008 I will have a proper look over what you have when I get a chance (see comment above too; I think flush-on-newline probably isn't the right choice).

mobin-2008 commented 11 months ago

Sounds good, I'm going to clear some codes and documention in 30 minutes.

mobin-2008 commented 11 months ago

@davmac314 I worked on it a bit and here's the list for pending things:

  1. istream class documention (global members are documented completely)
  2. fstream library documention (code is small and self explanatory but having documention is good)

I try to complete these things ~in 4 days~ when I get a chance

davmac314 commented 10 months ago

@mobin-2008

I try to complete these things in 4 days when I get a chance

Ok - please open a PR when you are ready. I had a quick look and the major concerns are gone, but I will still have a lot of comments/questions.

mobin-2008 commented 10 months ago

@davmac314 General design question: I really can't find any meaningful usage for copy/move constructor/assignment in all clases. Do you know a meaningful usage of those in ostream, istream and so on?

davmac314 commented 10 months ago

@davmac314 General design question: I really can't find any meaningful usage for copy/move constructor/assignment in all clases. Do you know a meaningful usage of those in ostream, istream and so on?

For std::ifstream/std::ofstream it transfers ownership of the underlying file handle and buffer. For standard streams it is the only way to transfer ownership of a file handle since there is no (standard) way to get the file handle from the stream object. It's probably not used a lot in practice though.

For your current design which has stream classes that are "heavier" in the sense that they include the buffer inside the object (which suggests to me that you really don't want to have multiple stream objects to deal with one underlying file/stream), moving between streams probably makes even less sense (especially because it should be possible to get the underlying file handle anyway).

mobin-2008 commented 10 months ago

I reworked move constructor/assignment section and it looks like c++ stdlib now:

  1. ostream/istream has protected move c/a.
  2. ofstream/ifstream has public move c/a which pass address of the buffer of old object to streambuf *buf

Hmm, But it is inefficient in memory because all of the new created objects will have a streambuf mainbuf by default which will wasted storage in moved objects: Example usage:

int main() {

    std::string str;
    dio::ifstream i2;

    {
        dio::ifstream i1("file");
        i1.open();
        if (!i1) return -1;

        i1.getline(str, '\n');

        i2 = std::move(i1);
    }

    i2.getline(str, '\n');

    return 0;
}

Debugger trace of i2 object

(lldb) print i2
(dio::ifstream) {
  dio::istream = {
    mainbuf = {
      cpbuffer<16384> = (buf = ""..., cur_idx = 0, length = 0)
    }
    buf = 0x00007fffffff6738
    fd = 3
    goodbit = true
    eofbit = false
    failbit = false
    badbit = 0
    eofreached = true
    throw_on_eofbit = false
    throw_on_failbit = false
    throw_on_badbit = false
  }
  path = 0x00005555555556e4 "file"
  are_we_open = true
}
(lldb) print i2.buf
(dio::streambuf *) 0x00007fffffff6738
(lldb) print *i2.buf
(dio::streambuf) {
  cpbuffer<16384> = (buf = "**********---***********\n*******- _   _ -********\n******-  0   0  -*******\n******-    |    -*******\n******-   ___   -*******\n*******-       -********\n**********---***********\n***********-************\n***********-************\n**********---***********\n*********-*-*-**********\n********-**-**-*********\n*******-***-***-********\n******-****-****-*******\n***********-************\n***********-************\n**********-*-***********\n*********-***-**********\n********-*****-*********\n*******-*******-********\n******-*********-*******\n"..., cur_idx = 50, length = 475)
}

Documention should advise that using std::move due to current design is memory inefficient and assigning object pointer to a istream * or ostream * is preferred.

@davmac314 WDYT?

davmac314 commented 10 months ago

I am confused by your debugger output, it doesn't seem to match the current code. I guess you didn't push the updated code?

It looks like you have added a streambuf abstraction wrapping the circular buffer, which is new. Also it looks like ifstream has both an internal streambuf (mainbuf) and a pointer to a streambuf (buf). Why does it have both?

davmac314 commented 10 months ago

(The reason std::ifstream can do a move is because it can transfer ownership of the streambuf. If the buffer is inside the stream object itself, there's no way to do that).

mobin-2008 commented 10 months ago

I pushed a new version and it's mostly about buffer's memory allocation. Now there is streambuf_mgr class and streambuf_allocator object which maintains all of streambuf used in the whole program. I tried to make them into safest status. Here is the description of this new class:

/* streambuf_mgr
 * =-=-=-=-=-=-=
 *
 * streambuf_mgr is buffer manager which provides functions about
 * allocating and deallocating streambuf buffers.
 *
 * All of streambuf_mgr functionaliy and interfaces are exception-free.
 *
 * copy/move constructors/assignments and destructor
 * -------------------------------------------------
 *
 * streambuf_mgr copy constructor is prohibited (copy constructor is deleted).
 * streambuf_mgr copy assignment is same as copy constructor.
 * streambuf_mgr move constructor is prohibited (move constructor is deleted).
 * streambuf_mgr move assignment is same as move constructor.
 *
 * streambuf_mgr destructor will deallocate all of the registered buffers.
 *
 * Public member: allocate_for()
 * -----------------------------
 *
 * allocate_for() is a function to allocate a streambuf and will return that streambuf
 * pointer. allocate_for will register all buffers in a std::list (and probably through
 * heap allocation) but the streambuf itself will be on stack.
 *
 * Public member: deallocate()
 * ---------------------------
 *
 * deallocate() is a function to deallocate an streambuf. This function will earse the allocated buffer
 * from allocated_buffers std::list.
 *
 * Public member: setowner()
 * -------------------------
 *
 * setowner() is a function to set the allocated buffer is for which object. this function will find allocated
 * buffer and change its "allocated_for" value to passed void pointer.
 */

...

// streambuf_allocator is designed to be responsible for all of buffer allocation
// in the whole program and you probably don't want to create another object
// from streambuf_mgr.
extern streambuf_mgr streambuf_allocator;

I think it's most memory efficient solution that we have.

mobin-2008 commented 10 months ago

Also ostream/istream can have constructor which has a custom streambuf* argument to bypass allocation from streambuf_base, In this case the user should ensure about availability of used buffer. e.g It's useful for cout, cerr and cin which are global objects in the whole program or on situations when there is chance of memory non-availability for heap allocation.

If it's ok, I will fix ostream description to represent these new cases.

mobin-2008 commented 10 months ago

@davmac314 I think the implementation is ready and just some documentation is missing (because I'm waiting to they get stabilized).

mobin-2008 commented 10 months ago

Sorry for spam :(

Last major change: diostates items are now more self-explanatory:

/* All stream state bits.
 *
 * "goodbit" means "everything looks ok" and has conflict with
 * other bits (e.g. setting eof as true will make "good" false).
 * "eofbit" means We are on End Of File and there is nothing left in buffer.
 * "buffer_failbit" means tried to use buffer when buffer point was nullptr.
 * "string_failbit" means there is failed operation at std::string related calls.
 * "posixio_failbit" means a POSIX I/O function failed and errno was set.
 *
 * rdstate() function returns stream's current status based on this.
 * setstate() function only accepts these values as its "bits".
 */
enum diostates
{
    goodbit = 0x01,
    eofbit = 0x02,
    buffer_failbit = 0x04,
    string_failbit = 0x08,
    posixio_failbit = 0x10
};

Also read() is public function for just reading and putting into the buffer without anything else.

davmac314 commented 10 months ago

@mobin-2008 can you explain the streambuf changes to me please.

Now there is streambuf_mgr class and streambuf_allocator object which maintains all of streambuf used in the whole program.

Why is that necessary? What is the benefit compared to the original design?

mobin-2008 commented 10 months ago

@davmac314

streambuf is just a cpbuffer with fixed size.

streambuf_base is a class which maintains a streambuf pointer and setbuf(), getbuf() functions to interact with that. It's designed to avoid duplication in ostream/istream.

When maintaining streambuf in class itself is expensive and has some problems around the std::move, streambuf_allocator shows itself, streambuf_allocator is designed to create and maintain buffers outside of the class itself.

davmac314 commented 10 months ago

When maintaining streambuf in class itself is expensive and has some problems around the std::move,

Do we need streams to be movable?

But alright, let's agree that it's probably better to keep the buffer outside the class because it's better to avoid large allocations on the stack. (And as a side benefit it's now feasible to move iostream objects, whether that's actually needed or not.)

However, what is the point of streambuf_mgr?

mobin-2008 commented 10 months ago

However, what is the point of streambuf_mgr?

streambuf_allocator is an instance of streambuf_mgr.

davmac314 commented 10 months ago

You are not actually explaining. I can see that streambuf_allocator is an instance of streambuf_mgr. Why are they needed?

davmac314 commented 10 months ago

What I mean is: why can't an iostream just allocate its own buffer? You can do dynamic allocation with new. Does using streambuf_allocator/streambuf_mgr have any advantage over that?

mobin-2008 commented 10 months ago

About the moving object looks like I misunderstood about what you said:

(especially because it should be possible to get the underlying file handle anyway).

I was thinking you meant there is should be move constructor anyway but looks like I was wrong.

Hmm, This is why streambuf_mgr exist: streambuf_mgr is a class to keeping streambufs outside of classes. It has a list to keep track of allocated buffers and provides deallocate() to deallocate allocated buffer from list.

If we don't have it, What can we do to keep buffers outside of classes?

davmac314 commented 10 months ago

If we don't have it, What can we do to keep buffers outside of classes?

I guess by "buffers outside of classes" you mean buffers outside of the iostream objects. That is achieved by having a pointer or reference to the buffer instead of a buffer instance itself. For example:

class iostream {
    streambuf buf; // buffer object inside iostream object
}

vs

class iostream {
    streambuf *buf; // buffer object separate to and outside of iostream object
}

You've already got the latter (buffer separate the iostream objects) in your latest design. However, to allocate the streambuf the code currently does:

        buf = streambuf_allocator.allocate_for(this);

My question is, what is the point of streambuf_allocator?

What is the advantage compared to:

        buf = new streambuf();

?

mobin-2008 commented 10 months ago

Right, I rewrited iostream buffers to use new/delete operator instead of a class everything sounds good (I really wasn't sure about new operator usage in this case)

buf = new(std::nothrow) streambuf(); or buf = new(std::nothrow) streambuf; ? (EDIT: Seems like they doesn't have any difference)

davmac314 commented 10 months ago

(I really wasn't sure about new operator usage in this case)

It is better just to ask, if there is doubt. And you should try to understand the reasons rather than just follow the rule without question.

In general in DInit we avoid doing dynamic allocation (eg new) because it can fail, and the failure must be handled correctly. But in this case your design avoided doing new by doing dynamic allocation via list.push_back instead. That can also fail, so it's not any better.

It's probably better to use unique_ptr and make_unique than plain pointers and new, in this case. These are better because they handle deallocation for you.

buf = new(std::nothrow) streambuf(); or buf = new(std::nothrow) streambuf; ?

These are equivalent.

Please fix up things like this before asking me to look at it:

// TODO: Has some bugs around returning garbage values in addition of needed value

davmac314 commented 10 months ago

(Fundamentally I think the design is probably ok now. I haven't looked too closely because it is hard to interpret the design from the code and I don't want to have to review the code many times. I will review it properly when it is actually ready, so let me know).

mobin-2008 commented 10 months ago

I can't understand what the hell just c++ standard did, unique_ptr exist in C++11 but make_unqiue is a C++14 feature!? Seems like there is not function about make_unique in dinit-util.h, I will make it.

mobin-2008 commented 10 months ago

Is it correct way to catch new failure?

        buf = std::unique_ptr<streambuf>(new(std::nothrow) streambuf);
        if (buf == NULL) buf = nullptr; // Memory can't be allocated for new buffer, Failed
davmac314 commented 10 months ago

Don't bother making make_unique if you don't need it.

is it correct way to catch new failure?

mobin-2008 commented 10 months ago

I refuse to use std::unique_ptr and std::shared_ptr in favour of more robustness because they can throw std::bad_alloc and trusting to try/catch block in low-level is hard because of C++ Itanium ABI, also I found sharing buffer between objects could be useful but shared_ptr use atomic operations for its functionality which is too expensive for dinit usage.

I think new(std::nothrow) and delete are most robust options. Current design is based on shared_ptr and I planned to use raw new/delete if you @davmac314 are ok with that.

davmac314 commented 10 months ago

I refuse to use std::unique_ptr and std::shared_ptr in favour of more robustness because they can throw std::bad_alloc

std::unique_ptr never throws bad_alloc.

The make_unique function can do that, but you don't need to use it in order to use unique_ptr. You can use unique_ptr together with new (nothrow), you don't need make_unique.

trusting to try/catch block in low-level is hard because of C++ Itanium ABI

Do you mean because throwing exceptions can require heap allocation? We already use try/catch/throw in Dinit so avoiding it just for this reason doesn't really make sense. It is anyway mostly a theoretical problem, especially for single-threaded programs. (I'm not saying you must use exceptions - I'm saying that I think the reason you gave for avoiding them isn't a good reason).

Current design is based on shared_ptr

That seems odd to me, what is being shared?

Edit: Oh I see:

also I found sharing buffer between objects could be useful

I don't understand this. How could this possibly be useful?

I think you should avoid changing the design at this stage. We have gone over it and got something that seems sensible. Don't change it now!

I planned to use raw new/delete if you @davmac314 are ok with that

I am ok with anything that makes sense and that uses the same principles used in the rest of the codebase (and which is idiomatic C++ unless there's a good reason to avoid that). Meaning: owning pointers should use unique_ptr unless there is a good reason not to.

iacore commented 7 months ago

I am reading the PR #263, and I wonder why libc isn't used for this purpose, since we already use snprintf from libc. musl libc's FILE* related API is a wrapper around POSIX API (with buffers too). I don't know about glibc's quality though.

davmac314 commented 7 months ago

I am reading the PR #263, and I wonder why libc isn't used for this purpose, since we already use snprintf from libc. musl libc's FILE* related API is a wrapper around POSIX API (with buffers too). I don't know about glibc's quality though.

Use of snprintf should probably be removed too. I don't want to be falling back to C interfaces, and anyway C standard I/O has some of the same problems as C++ standard I/O. This issue is about coming up with something better.

matu3ba commented 3 days ago

According to this reddit thread iostream will gete replaced with some work on the way. See also mapped file handle proposal https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p1883r2.pdf for C++26 and https://github.com/ned14/llfio.

Not sure, if any parts would be useful and how good the quality of the implementation is, because there is no CI and test coverage etc.