Can we test it? Yes, was can! - Mitchell Hashimoto - https://www.youtube.com/watch?v=MqC3tudPH6w
Hello, everyone. Thanks for coming in. I'll try to make it positive, I guess. I think it's optimistic, but let's just dive right into it. So this is something we've all heard before. You know, a PR is opened. Maybe it's trivial, maybe it's not trivial. Who knows? Maybe the author proactively said this, maybe they didn't. Maybe you ask why there aren't tests, but usually you. You'll see it. This word, this can't be tested, often followed by the phrase, but it's OK because I just verified it manually. I've said this before. You've said this before. Someone in this room has probably said this in the past 24 to 48 hours. It's fine. And the truth, however, is usually this can't be tested yet or. Or replace yet with easily. And easily is okay, because sometimes you can make the argument that something is could be tested, but the work involved in testing it is not worth it. Tons of stuff like that exists. Maybe if there's a catastrophic bug, it's not an issue. Maybe you're just running this in, you know, reproducible environments. It's not. Not a big deal. That's all fine. There's loads of those situations. This talk is not at all about when to test. I'll let you make those value decisions yourself. What I want to talk about is how things can be tested. So my goal with this talk is to really give you a tour of concepts and strategies that can be applied to a variety of different situations. Try to give you some, like, pattern matching capability and how you might be able to apply those. And I think it's very similar to. I think all of us in here would agree that there's. There's value in learning many different programming languages, even if you work every day with only a single one. Learning multiple different languages tends to make you a better programmer. Similarly, I think that just being exposed to different testing challenges, even if you may not directly hit the examples I show you, I think it'll make everyone a better tester. And so that's really my goal today. Okay, so you understand the goals of talk. Why should you sort of listen to me? What's my experience? I'll go through this relatively quickly. My background is I started a company called Hashicorp. We made a bunch of tools. I was part of all those initial engineering teams. So it's either I'm sorry or you're welcome. I don't know if you love them, feel free to tell me if you don't Love them then I didn't do it, it was somebody else. But yeah, I did that for about 12 years. And importantly, I think I was part of a lot of the initial testing laying out a lot of the initial testing strategies for this software and sort of some of the properties there, they tended to be network systems. Mostly there was a large scale in terms of usually nodes or that kind of scale, that dimension, those axes. And there was security sensitivity since one of the things we made was vault. More recently, I left Hashicorp at the end of 2023, if someone didn't know that. But more recently, for the past few years, I've been working on a project called Go See, which is a terminal emulator. It's not a company, it's just a passion project, it's just for fun. But it has dramatically different properties. This is desktop software. It's cross platform across Mac OS and Linux. It has GPU rendering in it. And I think this is interesting because each of these experiences has given me these dramatically different testing environments. And I'm someone who loves testing. So I wanted to figure this out and I want to share sort of how what I, what I've figured out over the past 15 years. All right, so going back in 2017, I gave a talk at Go for Con titled Advanced Testing with go. And as the title and venue may suggest, this is fairly GO specific or this was heavily. A lot of examples were GO here. But over the years a lot of people have come up. This is sort of one of the most watched talks I've ever given. A lot of people have come to me and said, even if I don't write go, I found value in watching this talk. And it's been almost 10 years and I felt that having a sequel to this talk would be a good idea because I've learned a lot more, I've experienced more complex situations and I was sort of cut short in this and I wanted to talk about more. But an important part of that is I'm trying not to overlap at all with this talk. So there's one concept that overlaps with this only because we're going to use it in the future. Things I talk about. But if some of the things I talk about maybe seem like I'm jumping into something advanced, this has a lot more. Even though this has advanced in the title, this has a lot more elementary concepts. And even if you don't write GO like that, if you like this talk, I think you would like watching that one as well. Okay, so let's start Getting into it, here's what we're taught, right? Like in school or in testing chapters of programming books or something like that. We're sort of taught that there's some operation, there's some output, you run it and you expect or assert that output. And some stuff is like this. And if the stuff, if what you're doing is like this, then great, it's easy, we all have a great time and generally we'll write tests. But the issue is that let me make sure it gets right. The reality is that in a lot of real world cases you might run into something where add is actually sub processing to a shell, running BC for some reason and you wanted it to work on Windows and now it doesn't work on Windows. This is silly. This is not reality. The only ecosystem that would possibly do this is probably JavaScript. But the whole point is that reality is actually messy, right? Like most tests really don't come down to that simple run a thing, get an output, you're happy situation. At least in my experience. A lot of the things that I actually care to work don't fall into that category. They fall into these categories where they have side effects or there's a complex state of the world that needs to be created in order to even run it in the first place. Networking, other devices like GPUs, concurrency and, and so on and so forth, the list goes on. And I'm not going to go into detail about this. I just hope that people agree and have noticed this in their own code. And a lot of these are what give rise to you work in one of these categories and say, hey, this couldn't be tested, but it works maybe. Ok, so the last thing I want to talk about, one more setup slide before we sort of get into it, is the two sides to testing. So there's two parts to testing. They're both equally important. This is also gonna be the slide format. So if the slide header starts with a test tube, we're gonna be talking about testing strategy. If the slide header starts with soap and bubbles, we're gonna be talking about testability. So testing strategy doesn't need an explanation. That's how to test something. Without this test don't exist. Everyone gets this one, it's easy. The second one is just as important. But the one that I find, regardless of engineering experience gets ignored more often is testability. If software isn't made testable, there actually are cases where you can say this can't be tested and you would be telling the truth. So you need to pair testability with actual testing strategy in order to be able to test everything. And the way you achieve testability generally are things like proper code structure, making the right APIs available, making something friendly for automation, things like that. So I'm gonna be talking about both of these intermixed in this again with the slide design guiding it in order to get us to be able to test more things. All right, so let's get started with our first testing strategy. Oh, another important point. The slides are roughly in order of simplest to more complicated or more advanced. So if you feel that some of this stuff is like, oh, this is obvious and this whole talk's gonna be obvious. I suspect it won't. I hope it won't. But based on earlier talk, this is a pretty advanced group, so maybe so, but let's get started with the first one. Snapshot testing. Sometimes this is also called golden files testing, ground truth testing, other things like that. Snapshot testing is. I used to call it some of those phrases, but I feel like snapshot testing is more of the norm nowadays culturally. So that's what I'm gonna call it here. The scenario where you want to do snapshot testing generally is when you have some sort of complex output format where programmatically coding the comparisons is difficult. That's, I think if you are familiar with snapshot testing, the one that you'd be familiar with a lesser discussed scenario. But I think possibly more powerful for snapshot testing is that it often gives you better diffs to understand why something failed. And I'm going to show you an example of this. So here's. Oh, this came out way smaller than I expected. Here's an example directly from Ghosty, my terminal emulator. When I previewed this locally, that image was really small and the code is really big. So I don't know. But the code, you don't need to read it. The code is zig. I'm going to use zig examples in this. This talk is totally language agnostic. It doesn't matter, so ignore that. But basically what we're doing here is go see has an embedded sprite font within the terminal emulator. There's a bunch of glyphs that terminals render that depend on the grid size to be perfect. The ones you're probably most Familiar with are PowerShell type glyphs, like the arrows. If you end up using a font. If you see this right now in your terminal, you use a font and the arrows don't quite perfectly match up with your grid size, then the terminal's not doing this. What Ghosty does is those glyph code points we programmatically rasterize, given the current grid dimensions, so they're always pixel perfect. This is not uncommon. A handful of terminals do this, but the issue is that how do you test this? How do we make sure it works? Specifically, one of the first things we found was that we had an off by one error when the grid size was odd in either dimension and we were one pixel off. And so one of the ways. Oh, and also new glyphs are introduced all the time. We've changed out the library of how we rasterize and things like that. How do we make sure we don't regress this stuff? This is a perfect case for snapshot testing. What we do is we embed a. We render a bitmap of every glyph that we programmatically rasterize, we commit that directly to the repository and we compare it pretty straightforward. That's the snapshot and that's how we compare. What the code was trying to show you there is that there's a basic step at the bottom where we read this ground truth, we run the actual rasterization and we just compare it. There's nothing fancy there. But one of the things I talked about is that one of the helpful properties of ground truth testing, snapshot testing is better diffs. And in this case, since our comparison is an image, we could apply standard image diffing techniques to this. And we do do this also in the repo. So when the test fails, we also generate the diff and dump it to the file system as part of the test run. And again, I thought this would be a little bit more readable. But what I did was I artificially modified our code to draw vertical bars, one pixel off to the right. It produced a failure. And the diffs on the right, it's a bit hard to see, but there are some green lines in there. That's the diff that gets generated. And the thing that I want to point out here is that this is a more helpful diff because prior to this we did have some tests and it would just basically say it didn't match or this pixel in this place was wrong. One thing that snapshot testing gives you is snapshots usually have more context than a single assertion. Like, yes, this one pixel is wrong, but here's all the pixels so you could see. And usually standard assertions in like a unit test type environment don't also include the greater context. And snapshots tend to so in this case, I could see very clearly that vertical bars are an issue. I could see it's across multiple glyphs. And me, with my experience with the code base, when I see something like this, immediately think some common function that draws vertical bars is wrong. And that's what I ended up, you know, breaking here. This doesn't just apply to images. Images make for an easy visual example. But for example, for Terraform, about a decade ago, one thing that we ran into was that the first step that Terraform does is builds a resource graph that gets executed. And we had a bunch of tests around building that graph and we would get failures saying this expected node, this expected edge doesn't exist and it would be very difficult. We were spending a lot of time as engineers figuring out what the graph changed. So one of the things I changed was even though it wasn't hard to program those assertions, the diffing was very hard for us. So I ended up starting to dump the expected graph and the graph we got and then doing generating a dot format that actually colored the edges that were missing red, the bonus edges, green vertices, so on. Then you could load the whole graph and debugging these things became much, much easier. That was actually a text diffing format. You could render that into an image, but that was just text stiffing. But again, since we're missing one vertice, but we could see all the vertices, the greater context around it, debugging became easier. So I think this is actually the bigger benefit of snapshot testing, but something to keep in mind. Okay, this next section I didn't mean to keep in here, so I'm actually gonna skip it. That one overlaps as well with the GopherConTalk, so you could look into that one. It was also kind of like an opinionated one that people tend to get upset about. So it's fine. We should skip it anyway. Okay, let's talk about a testability subject. So soap and bubbles isolating side effects. I actually think if, if there's no other part of this talk that you pay attention to, this is the number one force multiplying strategy that sort of exists to make code testable. This comes up over and over in different contexts. And the scenario is this you have some sort of behavior you want to test, but it's reliant on some sort of external IO or otherwise complex system that's sort of filled with side effect type behavior. And this is a really common case of this can't be tested. And what you're looking for, the Goal with this is within this I O complexity state soup, you're trying to find the purely functional behavior stuck in there, extract it out, reorder things in order to get something that you could mostly test. In this case, you could usually get most of the complexity tested as a result of this. What you get is something that is obviously testable. I'm going to show you an example of this. You get something that's obviously testable, but it becomes a garbage in, garbage out type of test. And what I mean by that is you can't in this case simulate the external side effecty things. So you're gonna artificially provide those inputs. And so if you provide garbage and you test garbage, you're gonna get garbage out. You need to make sure that the inputs that you're gonna be providing to these sorts of tests are actually realistic in real world. And again, that's conceptual. Let's see it in practice. Okay, so here is something. We're gonna start visually and then see code, but I actually doubt we're gonna see any code given the slide sizes. But let's try to understand this visually. Here's a simplified but real example from Go see Again, one of the things a terminal emulator has to do is probably the main thing that it has to do is when you press something on the keyboard, it has to encode that into some format which is then sent to your shell or whatever running program. And then it does whatever basic things, like if you press Control R at your shell prompt, you tend to expect a reverse search to show up, so on and so forth. Early versions of Ghosty had a keyboard input handler that looked like the above image. Basically, what would happen here is a user would press a key, we would read mouse state, which seems odd, but depending on mouse state, it actually affects whether a key should be encoded at all. Like if you're actually actively highlighting something and you press certain keys, you might want to move the highlight, shift the highlight, things like that. So we would first grab mousetate, check, respond in some way. Then we would check keyboard state, because we need to know what keys are pressed, what modifiers are pressed. Is it a repeat, is it a first press? You know, things like that, do stuff around that. Then we would read terminal settings. There's a variety of settings that affect how it's encoded. Are we doing legacy encoding, kitty encoding within each of those? Are we encoding alternate Unicode code points? Are we encoding control as differently? There's a bunch of stuff. And then finally we encode the key and then write it out to the actual PTY that we have. And this was untested because setting up mouse keyboard state, stuff like that is non trivial. And I originally approached this as oh, this isn't a testable thing. And I punted it to a full end to end test. I figured one day I would probably spin up a VM or something, synthesize inputs and assert something. I just punted it away. We'll figure it out later. But then I sort of got punched in the face enough with this code. Constant regressions happening here. A lot of complexity that I realized I had to do something, even if that something was that VM based test right now. And I actually sat down and focused on I need to make this testable because this can't go on. And what I realized is what these colors are showing. If you could see the colors, the yellow is the stuff that's dependent on external logic and the green is the stuff that, that doesn't need any, is sort of pure. It just has some inputs and gives you a set of outputs and doesn't touch any external systems. And it's really easy to see here because it's colorized. And I simplified it into distinct categories of function calls and also made it alternating. But hopefully people could understand that the reality of this function at the time was that all of this was intermixed and we would grab state when we needed it and run conditionals on it. And it wasn't, at least to me. It took me a few hours of really staring at this code to see the shape of suddenly something emerge. And what I saw emerge was this at the bottom. If I actually took all of this stuff that grabbed external state, moved it to the top, turn into a read process, write order, then I could isolate that, provide artificial inputs there, test this green thing that is pure, just has inputs and gives you an output and then it becomes that testing 101 expect add one plus two equals three sort of environment. And in this case most of the complexity, most of the bugs, most of the issues was in this green thing. So we were able to really dramatically eliminate a bunch of issues. And also that made it much easier to fuzz and things like that. And I'm not going to talk about fuzzing in this. There's enough of that here. Yep, that's what I expected. This is the actual code from the key encoder. I wish you could see the bottom. The bottom is more important, but the top just know that each line, I think you could all see lines, just not the text that's in the line. Each line is a piece of state, whether it's structure, boolean, an integer character. It's a piece of state that's needed to do the key encoding. And what I'm just trying to visualize here is how much state is actually required for a terminal to produce a valid key encoding. There's something like. In total, there's something like 15 different fields here. Some of it is produced by the operating system, some of it's produced by the terminal, its internal settings. But the bottom is an actual test I copied out directly just verbatim zero edits to show the types of regressions that we can now test against and what the bottom test is doing. Just believe me, and you could look at the slides later. It's testing how we encode a certain input from a Russian keyboard layout with kitty keyboard settings encoded to also add alternate Unicode characters and predicts expects we get the right thing. This is the reason I had to test this because every time I would fix a feature or fix a bug or implement a feature, I would regress some to me very foreign layout that I of course was not just running adult speaker type Russian. And also getting this specific kitty layout is very difficult. So you know, this is very common. There's Russian, Japanese, Chinese, Hungarian, like all sorts of very language specific test cases in there to make sure we constantly do the right thing. And I think because of this, Go See has one of the most complete and stable sort of encoding key encoding features out there. And so this helped a lot. And this is sort of the key point of isolating the side effects. And basically every section from here on out is going to continue to show examples of this. We're going to take snapshot testing, we're going to take isolating side effects and we're going to bring them together to do something more and more complicated. All right. GPUs. What a crazy time for GPUs. So thankfully I don't work at OpenAI, so I'm not going to talk about AI at all in this talk. I'm just going to talk about GPU programming in general. It could apply to AI, but in my case it applies to rendering and some compute. But I want to talk about GPU programming and GPU testing actually. So background Ghosty is a GPU rendered terminal emulator. And what that means is when you run the terminal, that main thing you see with your cursor blinking, that whole thing is just an image that I'm rendering via the gpu. We also do A little bit of compute on the gpu. It was my personally, it was my first foray into writing any kind of GPU code, starting a few years ago. Prior to that, I lived purely on the cpu. And given that, when I started coming into it, one of the first things I asked was, all right, how do I test this? My initial feeling and response was the classic can't be tested. This seemed like a very obvious thing that I should spin up a VM and take screenshots of. I was rendering after all. So that seemed to be the right solution. But for years our renders didn't have unit tests. And for years, similar to key encoding, we would just whack a mole regressions constantly in the renderer, in the GPU code. And so finally I sat down and said, I need to figure this out. Resources on how to test GPU logic is surprisingly scant. Like if you do a web search of how to test GPUs, it's one of those rare things where Google the first page is just completely garbage. There's one response that has an idea that's kind of interesting, but it was just an idea that no one implemented. So it kind of leads you in a direction that you then have to figure out on your own. I went from that to going to the Kronos Group, Apple and Microsoft and downloading the reference material for DirectX, direct 3D, metal and OpenGL, Vulkan and all their docs, anything they could provide me in big text format. And I did command F searching, LLVM assistance searching on that. Everything I looked for test verify, snapshot, bug stability. I looked for all these terms and the amazing thing is the total reference material across those three vendors for their language specification, driver specifications and so on is something like 4,000 pages of PDF. And I didn't get a single hit on any of those terms. Like the word didn't pop up one time. So as far as I could find, there are zero official resources on how to do testing with GPUs, and very, very few people have even cared to ask the question, at least publicly, to where it was indexed. So that's where I was left. I took that mostly as a challenge to see if. Well, I feel pretty good about testing. I like testing. Maybe I could figure this out. And I think I did at least something that works for me pretty well. So this is where I'm just going to add a quick disclaimer that you could probably tell based on my experience here, that I'm not an expert on GPUs. I never worked on complicated 3D games. My terminal emulator is one of the only things beyond like toy advent of code examples that ever used a gpu. I'm not quite sure that my techniques here actually generalize that well. In my defense, Ghostly does have about 15,000 lines of renderer code split across metal and OpenGL. We have separate renderers for both systems, and that includes both the CPU code and the shaders for the GPU. The shaders are about 2,000 lines. So 13,000 on the CPU, 2,000 on the GPU. That's what I was working with. That's what I was trying to test. The core realization I came up with here is that GPU programming requires two sides. I'm going to go into that background. There's the CPU side to prepare the data and then process the results usually. And the GPU part that actually runs the shaders. And I felt that we can test each of those in isolation and specifically really clearly a GPU is just a pure function evaluator, which makes testing really easy, but setting up the workloads really hard, which is kind of a funny thing to run into. But let's go ahead and look at this more visually and start with the GPU side, the CPU side. We could actually read this basic thing. So as a point of background, for those less familiar with GPU programming, the CPU does have to do some work to prepare the gpu. The CPU has to put together the right data, the right steps, basically these little job descriptions that it then eventually offloads and submits the GPU and says, here's a bunch of crap, go do it and I'll come back, or you tell me when you're done, I'll read this later. That's the really general way to think about a gpu. The work the CPU does in preparation for the GPU can be roughly thought of as this top function. It's a function that takes in some sort of state of the world. If you're doing rendering, this might be called the scene state. It's what monsters are on the level. Where are they? Where's the camera? Where's the player? What planet am I on? That's the scene state that exists. It brings in the scene state, and as a result it produces three sets of values. It produces a graph, so you get vertices and edges, and it produces data attachments for that graph, which nodes need which access to what data. And that's a gpu. The graph is just a graph. It has vertices, edges, and the vertices are Operations, right? It's stuff like vertex shading, fragment shading, compute shading, things like that. Edges are the data dependencies between the steps. The vertex shader is going to produce some sort of output that the fragment shader needs to bring in, or the vertex shader needs, the fragment shader needs access to a certain texture, things like that. There's these edges that exist in the graph and the data attachments are literally byte buffers. Historically you'd probably call these textures. More modern graphics APIs out there, Vulcan, Metal and later direct 3D, they all tend to really just call these buffers now because it's really just a set of bytes. Sometimes the bytes have structure to them. They're RGBA with dimensions and stride and you have a texture. But sometimes they're just bytes because you're just computing stuff. So data is just bytes and therefore you could do whatever you want with it. So to test the CPU side, we have to apply that technique of isolating the side effects. In this case, the side effects are all the API calls to the GPU itself in order to submit, prepare and submit this workload. And so what I ended up doing was creating an intermediary where we bring in the scene state, we produce the graph and the data, and, and then I assert that the graph has the right shape, which I have a bunch of experience with the terraform and the data. I do snapshot testing, bringing that back because it's just bytes. And so we're able to do some structured sort of snapshot verification on that. And then after that there's a small amount of simple untested code which just translates the graph and the data attachments into GPU API calls. That stuff never really changes. I'm happy to keep it untested until there's end to end tests. And in this way we're able to test that given a certain scene state, we're producing the right workloads for the gpu. But we're not sure the GPU is going to do the right thing with that. But this is still one big part of the equation. I mean, this is 13 out of the 15,000 lines of code that go see does in order to render a scene. So this is a big one. Then on the GPU side, it's visually much simpler. GPUs have no access to disk, they have no access to networking, they, they have no access to any other peripherals, they only have access to their own memory. And so a GPU by definition is pretty much just a pure function evaluator it has some sort of computation, it has data and it outputs data. And that's something that's really juicy to test, really easy to test. But the funny thing about it is, like I said, the hard part is actually submitting the workload. So the general idea with the GPU side is I want to artificially construct some set of input buffers that my pipeline's going to expect. I'm going to ensure the output buffers are CPU readable. So instead of writing to a frame buffer that might never come back to the cpu, it's going to render this screen. Don't render the screen, render to this other CPU readable memory I have. Then I submit actual GPU work. We run unit tests on the cpu. On the cpu, we're going to run unit tests for the GPU on the gpu, submit the actual GPU work and then compare the output buffers. Just hand waving, you know, snapshot testing, actually parsing the data, whatever you want to do, but compare the output in some way. That's the general idea. It's hard, right? So in practice, what I found and what I've really only gotten to work well enough that I've shipped it, is full render passes with snapshot testing. What I do is I artificially create the scene state, usually a very small terminal, like a two by two terminal. I send it to the actual gpu, I get an image out and then I compare images and I expect pixel byte, equivalent images. It sounds kind of like end to end testing. It is in a certain way, I would say it's not quite a unit test, it's more of an integration test, but it's very, I think it's still a very robust, powerful test for two reasons. One, it's much faster. So getting a window made, submitting GPU work, grabbing a screenshot and, and comparing it is instant on any modern computer. It's really, really fast and it is very robust. In this case, we're really tightly controlling the input scene state. We're running one render pass, we're grabbing exactly one frame of results out of the other side and comparing against it. We don't have to worry about standard end to end tests with timing and synthesized inputs and, you know, window positions and chroming and like all this other stuff around the edges, like we get exactly the image perfectly cropped for what we're trying to compare and it's a single frame. So it's very robust. It works. For the future, I do want to test shaders in more isolation and I've already done a bunch of this. I'm going to kind of go through a few of the things I've done as proof of concepts and they work, but they all have these trade offs I'm not quite happy with. So I haven't actually shipped this in any way. So that's my disclaimer for this, that the proof of concepts all do work though. So the full render pass obviously tests a full input to image, but individual shaders themselves have quite a lot of complexity, or can have quite a lot of complexity. There's conditionals, there's loops, there's obvious edge cases that I see that I want to test in some way. And it's sometimes hard to elicit those edge cases through an initial scene state, or at least to visualize them in a resulting output state. So I want to get closer to a unit test with shaders. And so to do that I've been trying a variety of techniques, trying to figure out the best way forward. Again, these are all things that I haven't found a lot of people trying that much. I'm not going to talk about each in detail because I'm still learning quite a bit about it, but I'm going to just cover the high level ones. People know more details about this, I'd love to hear about it. But for example, sort of just a couple concepts. OpenGL. I know a lot of people are hyped about Vulkan and things like that, but OpenGL still works. OpenGL has this feature called transform feedback and what it basically allows you to do is capture the output of some shaders, some types of shaders, not all of them into a CPU readable buffer. It's actually a really nice API because you literally just add strings, say the variables that you want, and it just grabs them and throws them in order into an output buffer. And that's perfect. I don't know what this was made for, I don't know, I've never seen it. I did a source graph search and things like that. I don't see it used as very few API calls. It doesn't really exist in Vulkan, so clearly there wasn't enough value to move it forward. But it is kind of perfect for testing some kind of shaders. And that's an issue. It only applies to some on the metal side. Metal doesn't have transform shaders, neither does direct 3D. So what I found I've had to do there is extract shared logic into compute shaders. So non rendering, just compute shaders, make Each side just call this shared library and then run it through a compute shader and kind of build my own transferred feedback mechanism. That requires a lot more code. That requires code restructuring of GPU code, which standard for testing, but isn't great. And it works. The nice thing about that is that works for every kind of shader, no matter what. But you have to be able to extract it into a compute shader, which I've never found you can't do. So there's a lot of promise here. I unfortunately just didn't get to a production state of the future side. So I don't know, hopefully one day I could blog or talk about it and have it all figured out. But I think the result of this is I feel confident that we're now able to test our renderers. The full render pass thing I have is very robust and fast. And you don't actually need a GPU like hardware to do it because you could run it against software drivers. We could assume the drivers work, that we're not trying to verify drivers here, so just run it against software drivers. And again, we're just running one frame. So it's super fast. And yeah, it's interesting and I think it highlights snapshot and isolating side effects really well. Okay, the last sort of topic I want to talk about is VM testing. There are some things that do end up requiring this specifically to test those yellow boxes that I had earlier. The only way to really do it is to really make it happen. And the only way to simulate things like keyboard and mouse and other types of events is through things like VMs, not just keyboard or mouse. Right. This is also network failure stuff that antithesis is really good at. Disk failures, things like that. It's sort of best done through a hypervisor layer. I'm gonna apologize here because we're gonna mention nix. And I know a lot of people feel that Nick's enthusiasm is pretty exhausting and don't want to hear about it. So I am sorry. In my defense, I feel pretty confident that I have a good grasp about dev test environments, virtualization, containerization. Like it was my whole career for like 15 years. And I don't know any other technology that could achieve what I'm about to show you very well. So I'm going to use nix and I'm going to. And I'm sorry, but not sorry at the same time. Okay, so let's first talk about VM testing without the nix part. Okay, so if you're like having an emotional reaction, we'll start here. Okay. There are some things, like I said, that just require an end to end test. And VMs are the best for that. You can maybe get away with containers, but just use them interchangeably if you want. But in this case, I'm just keep using the word vm. VM testing lets you model really complex, pretty much arbitrarily complex states of the world. Specific kernel software versions, specifically the interplay between those. For me, on the desktop side, it lets me simulate different locales, different keyboard layouts, excuse me, things like that. And you sometimes just need them. So the idea is that you spin up a full vm, you actually run software, synthesize events, and then somehow assert that what you wanted to happen happened. That's usually through screenshots or SSH commands. You know, SSH commands would be like, this process is running, this file exists, this file has this contents, whatever. But those are usually the two mechanisms you do it. Okay, now we bring in the nix. Why do we have to bring in the nix? I'm glad we could read this actually. So I'm bringing in Nix because Nix provides a full first party testing framework that has access to Nix. And these three properties are really important because first party, Nix actually uses this to test Nix itself. And so it's not going away. It's running every day. It's running right now. There's thousands of jobs queued up right now by the next project in order to run these types of tests. Second, it's an actual test framework. It doesn't just define how to spin up a vm. It has a full API for writing tests and asserting they pass. And that's important because it's not just like run a Docker image or something like Docker provides the runtime for a container, but it's not gonna give you any of the tools to actually test in that case. This is giving you both sides of the equation. And then third, oops, sorry, go back one. Since it is powered by Nix, this is a benefit because you get full access. Well, the language is probably a detriment, I'm gonna be honest. But the access to Nix packages is a benefit because you get access to basically every version of every piece of semi popular software that has existed for the past decades. And this is really important because you could pin specific versions of everything down to kernel libc, everything. So this is the only way I've found, when the most annoying desktop users of all time, Debian users, bring an ancient version of long term support software and say, this thing doesn't work. This is the only way I've been able to actually verify it works. So let's take a look at what this looks like. The first step for any VM test is actually define the machine. I put optional pluralization because Nix lets you define multiple machines and do networking between them. We're not going to talk about that. That's like the whole talk. It's probably like a whole degree for machine configuration. It's just. And these air quotes are doing a lot of work. It is just the Nix OS configuration. You could put anything in there that you would configure a full NIX installation with. So that means, like I said, that means anything. Kernels, drivers, users, packages, everything. Step two is actually defining the tests. The tests are written in Python, they're not written in nix. So you actually put a string or embed a file with your tests here and Nix gives you this full Python API that gives you some nice high level stuff like waiting for systemd units. Since Nix OS uses systemd. Actually you could even do ocr. You could wait for certain text to appear on the screen and it just handles that for you. And it's just Python. And one of the things you get out of this is you actually can access a repl, so you could have the VM and just use the REPL to be playing around with your tests. So that's good. In this case we're defining a test that the ALICE user. I forgot to mention this, a previous one, we installed Firefox for Alice. The ALICE user has Firefox and Root does not have Firefox, obviously a toy example, but you could do anything here. And then step three is you have to run them again, hand waving going on here, as you have to do whenever you talk about Nix. But basically the important thing is you have a mechanism built in to the framework runtime to run the tests, to get a repl, to debug the tests, to develop the tests, to run a single test. Everything is sort of like there for you within a single command of some sort. And so this is just a full end to end thread thing in order to handle VM tests. And like I said, I just haven't found any other thing that's focused on providing this level of flexibility with a focus on testing. Right. There's tons of frameworks, I built some of them to spin up machines, spin up VMs, but not to just complete the end to end part of it in practice. What am I actually using Nixos VMs for? And again, stuff you really can't test without a full sandbox environment. The thing that really triggered me, the thing that really made me do this was input methods. I'm a fairly calm person, but input methods on Linux made me pound the table a few times. For those that don't know, an input method is basically any sort of how you input sort of certain Asian languages, emoji keyboards are an input method. It's the ability to input any character that's not represented on your physical keyboard is an input method. Handwriting as well as an input method. On Linux, the input method, the input method framework, the windowing system slash compositor, they're all developed by different people. And this is where I think the struggles of Linux really shine. Because of this very specific versions of different things just behave wildly differently. And it drove me crazy. So I needed to use VM testing to test input methods. I love Linux, I use NEXT os, but that's in contrast to something like Apple where you could really clearly tell there's some vertical mandate that all these things must work together in lockstep. So much easier as an app developer, but, you know, it's what I have to work with. And then there's other stuff, sort of desktop integrations, making sure that Open and Ghostly appears on right click. How can you possibly test that without actually like taking a screenshot and right clicking and taking a screenshot? These are things that also just break constantly in Linux with various version upgrades. So perfect for VMs. There's a lot of complaining about Linux up here, but it's just like this is the work you have to do to make, in my opinion, a stable desktop app experience for desktop Linux. I'm just going to put this here for later. Take a picture or just download the slides later. Here are resources where you could actually learn a lot more about VM testing. It's extremely powerful, but like everything in Nyx, the learning curve is like a sheer vertical cliff. So, you know, if you want to traverse the wall from Game of Thrones, then this is the resources that you're going to need to use. I think the benefit is the payoff from doing this. For the right type of testing that you need, there's nothing that compares. And so that's it. Thank you. I know we only covered five topics here, but I think it was a lot. Most importantly, again, if we got nothing else out of it, isolating side effects is a super powerful technique and I wanted to show multiple examples of that. And that's what I tried to do. And if you want more, see the go for contact from 2017, so thank you.
Can we test it? Yes, was can! - Mitchell Hashimoto - https://www.youtube.com/watch?v=MqC3tudPH6w
안녕하세요, 여러분. 와주셔서 감사합니다. 긍정적으로 말해보겠습니다. 낙관적이라고 생각하지만, 바로 본론으로 들어가죠. 이것은 우리 모두가 전에 들어본 적 있는 내용입니다. PR이 열렸을 때, 사소할 수도 있고 아닐 수도 있죠. 작성자가 선제적으로 말했을 수도 있고 아닐 수도 있습니다. 테스트가 없는 이유를 물어볼 수도 있지만, 보통은 이렇게 들립니다. "이건 테스트할 수 없어요"라는 말을 듣게 되죠. 그 뒤에 보통 "하지만 괜찮아요. 수동으로 확인했거든요"라는 말이 따라옵니다. 저도 이런 말을 한 적 있고, 여러분도 그랬을 겁니다. 이 방에 있는 누군가는 지난 24~48시간 동안 이런 말을 했을 거예요. 괜찮습니다. 하지만 사실은 보통 이런 겁니다. "아직" 테스트할 수 없거나... 또는 "아직"을 "쉽게"로 바꿔 말할 수 있죠. "쉽게"는 괜찮습니다. 때로는 뭔가를 테스트할 수 있다고 주장할 수 있지만, 테스트하는 데 필요한 작업이 가치가 없을 수도 있습니다. 그런 경우가 많이 있죠. 치명적인 버그가 아니라면 문제가 되지 않을 수 있고, 재현 가능한 환경에서만 실행한다면 큰 문제가 아닐 수 있습니다. 그건 다 괜찮습니다. 그런 상황은 많이 있어요. 이 강연은 언제 테스트해야 하는지에 대한 것이 전혀 아닙니다. 그런 가치 판단은 여러분이 직접 하시기 바랍니다. 제가 말하고 싶은 건 어떻게 테스트할 수 있는지이번 강연의 목표는 다양한 상황에 적용할 수 있는 개념과 전략들을 소개하는 것입니다. 여러분에게 패턴 매칭 능력과 적용 방법을 알려드리고자 합니다. 이는 매우 유사한 것 같습니다. 우리 모두가 동의할 수 있듯이 가치가 있습니다. 매일 하나의 언어만 사용하더라도 여러 프로그래밍 언어를 배우는 것은 가치가 있습니다. 여러 언어를 배우면 더 나은 프로그래머가 되는 경향이 있습니다. 마찬가지로, 다양한 테스팅 과제를 접하는 것만으로도, 제가 보여드릴 예시를 직접 마주치지 않더라도, 모두가 더 나은 테스터가 될 수 있다고 생각합니다. 그것이 오늘 제 목표입니다. 자, 강연의 목표를 이해하셨죠. 왜 제 말을 들어야 할까요? 제 경험은 무엇일까요? 간단히 설명해 드리겠습니다. 저는 Hashicorp라는 회사를 시작했습니다. 우리는 많은 도구를 만들었고, 저는 모든 초기 엔지니어링 팀의 일원이었습니다. 죄송하다고 할지 아니면 감사하다고 할지 모르겠네요. 좋아하신다면 말씀해 주세요. 싫어하신다면 제가 한 게 아니라 다른 사람이 한 겁니다. 네, 약 12년 동안 그 일을 했습니다. 중요한 점은, 이 소프트웨어에 대한 초기 테스팅 전략을 수립하는 데 많은 부분 참여했다는 것입니다. 대부분 네트워크 시스템이었습니다.일반적으로 노드 수나 그런 종류의 규모, 차원, 축의 관점에서 대규모였습니다. 그리고 보안에 민감했습니다. 우리가 만든 것 중 하나가 볼트였기 때문입니다. 최근에 저는 2023년 말에 Hashicorp를 떠났습니다. 혹시 모르는 분들을 위해 말씀드립니다. 하지만 최근 몇 년 동안 Go See라는 프로젝트에 참여해왔습니다. 이는 터미널 에뮬레이터입니다. 회사가 아니라 그저 취미 프로젝트로, 재미로 하는 것입니다. 하지만 이는 매우 다른 특성을 가지고 있습니다. 이것은 데스크톱 소프트웨어입니다. Mac OS와 Linux를 아우르는 크로스 플랫폼입니다. GPU 렌더링을 포함하고 있습니다. 그리고 이것이 흥미롭다고 생각하는 이유는 이 각각의 경험들이 제게 매우 다른 테스팅 환경을 제공했기 때문입니다. 저는 테스팅을 좋아하는 사람입니다. 그래서 이것을 파악하고 싶었고 지난 15년 동안 제가 알아낸 것을 공유하고 싶습니다. 좋습니다. 그럼 2017년으로 돌아가보겠습니다. 저는 Go for Con에서 "Advanced Testing with Go"라는 제목의 강연을 했습니다. 제목과 행사에서 알 수 있듯이, 이는 상당히 GO에 특화되어 있었습니다. 많은 예시들이 GO와 관련된 것이었죠. 하지만 수년에 걸쳐 많은 사람들이 다가왔습니다. 이는 제가 한 강연 중 가장 많이 본 강연 중 하나입니다. 많은 사람들이 GO를 쓰지 않더라도 이 강연을 보고 가치를 찾았다고 말했습니다. 거의 10년이 지났고, 저는 이 강연의 후속편을 만드는 것이 저는 더 많이 배웠고, 더 복잡한 상황을 경험했습니다 그리고 이 부분에서 짧게 끝났는데 더 말하고 싶었습니다 하지만 중요한 점은 이 강연과 겹치지 않으려고 노력한다는 것입니다 그래서 한 가지 개념만 겹치는데, 이는 앞으로 사용할 것이기 때문입니다 제가 말씀드리는 것들이 조금 고급스러워 보일 수 있습니다 하지만 이 강연은 더 많은 내용을 다룹니다. 제목에 고급이라고 되어 있지만 이 강연은 더 기초적인 개념들을 많이 다룹니다. 여러분이 GO를 그렇게 작성하지 않더라도, 이 강연을 좋아하신다면 저 강연도 보시면 좋아하실 것 같습니다. 자, 이제 본론으로 들어가겠습니다. 우리가 배운 것은 이렇습니다. 학교나 프로그래밍 책의 테스트 챕터에서 배운 것처럼요 어떤 연산이 있고, 출력이 있고 실행하면 그 출력을 예상하거나 단언합니다 일부는 이런 식입니다. 그리고 만약 여러분이 하는 일이 이와 같다면, 좋습니다. 쉽고 우리 모두 즐겁게 테스트를 작성할 수 있죠. 하지만 문제는 제가 제대로 말씀드리고 있는지 확인해 보겠습니다. 현실은 많은 실제 상황에서 여러분은 다음과 같은 경우를 만날 수 있습니다 add가 실제로는 셸에 서브프로세스를 실행하여 BC를 실행하고 어떤 이유로 Windows에서도 작동하길 원했지만 작동하지 않습니다 이건 말도 안 되는 얘기죠. 현실이 아닙니다. 유일한 이것은 어리석은 예시입니다. 현실과는 거리가 멉니다. 유이를 가능하게 할 생태계는 아마도 JavaScript일 것입니다. 하지만 핵심은 실제로는 상황이 복잡하다는 것입니다. 대부분의 테스트는 단순히 무언가를 실행하고, 결과를 얻고, 만족하는 상황으로 끝나지 않습니다. 적어도 제 경험상 그렇습니다. 제가 실제로 신경 쓰는 많은 것들이 그런 범주에 속하지 않습니다. 오히려 다음과 같은 범주에 속합니다. 부작용이 있거나 실행하기 위해 복잡한 세계 상태를 먼저 만들어야 하는 경우입니다. 네트워킹, GPU와 같은 다른 장치들, 동시성 등 목록은 계속됩니다. 이에 대해 자세히 설명하지는 않겠습니다. 다만 여러분도 동의하고 자신의 코드에서 이를 경험했기를 바랍니다. 이런 것들 때문에 이런 범주에서 작업하다 보면 "이건 테스트할 수 없지만, 아마도 작동할 거야"라고 말하게 됩니다. 좋습니다, 마지막으로 얘기하고 싶은 것은, 본격적으로 들어가기 전 마지막 준비 슬라이드입니다. 테스트의 두 가지 측면입니다. 테스트에는 두 부분이 있고, 둘 다 똑같이 중요합니다. 이것이 슬라이드 형식이 될 것입니다. 슬라이드 제목이 시험관으로 시작하면 테스트 전략에 대해 이야기할 것입니다. 만약 비누와 거품으로 시작하면 테스트 가능성에 대해 이야기할 것입니다. 테스트 전략은 설명이 필요 없습니다. 그것은 무언가를 테스트하는 방법입니다. 이것 없이는 테스트가 존재하지 않습니다. 모두가이것은 쉽습니다. 두 번째도 마찬가지로 중요합니다. 하지만 제가 보기에 엔지니어링 경험과 무관하게 더 자주 무시되는 것은 테스트 가능성입니다. 소프트웨어가 테스트 가능하지 않으면, 실제로 이것은 테스트할 수 없다고 말할 수 있는 경우가 있고 그것이 사실일 수 있습니다. 그래서 모든 것을 테스트할 수 있으려면 테스트 가능성과 실제 테스트 전략을 함께 고려해야 합니다. 테스트 가능성을 달성하는 방법은 일반적으로 적절한 코드 구조, 올바른 API 제공, 자동화에 친화적인 설계 등입니다. 그래서 이 두 가지를 혼합해서 이야기하겠습니다. 슬라이드 디자인을 통해 더 많은 것을 테스트할 수 있도록 안내하겠습니다. 이를 통해 더 많은 것을 테스트할 수 있게 될 것입니다. 자, 첫 번째 테스트 전략부터 시작해보겠습니다. 아, 한 가지 중요한 점은 슬라이드가 대략 가장 간단한 것부터 더 복잡하거나 고급스러운 순서로 되어 있다는 것입니다. 이 내용이 너무 뻔하다고 생각하시거나 전체 강연이 뻔할 것 같다고 생각하실 수 있습니다. 그렇지 않길 바랍니다. 앞선 강연을 보면 이 그룹은 상당히 고급 수준이니까요. 하지만 시작해보겠습니다. 첫 번째는 스냅샷 테스팅입니다. 때로는 골든 파일 테스팅, 그라운드 트루스 테스팅 등으로도 불립니다. 저는 예전에 그런 용어들을 사용했지만, 요즘은 스냅샷 테스팅이 더 일반적인 것 같아서 여기서는 그렇게 부르겠습니다.08:일반적으로 복잡한 출력 형식이 있어서 프로그래밍 방식으로 비교 코딩이 어려운 경우입니다. 스냅샷 테스팅에 익숙하다면, 하나의 덜 논의되는 시나리오에 익숙할 것입니다. 하지만 제 생각에 스냅샷 테스팅에서 더 강력할 수 있는 점은 종종 뭔가가 실패한 이유를 이해하는 데 더 나은 차이점을 제공한다는 것입니다. 이에 대한 예시를 보여드리겠습니다. 여기 있습니다. 오, 예상보다 훨씬 작게 나왔네요. 여기 제 터미널 에뮬레이터인 Ghosty에서 직접 가져온 예시가 있습니다. 로컬에서 미리보기했을 때 이미지가 정말 작았고 코드는 정말 컸어요. 그래서 모르겠네요. 하지만 코드는 읽을 필요 없습니다. 코드는 Zig입니다. 저는 이 발표에서 Zig 예제를 사용할 겁니다. 이 발표는 완전히 언어에 구애받지 않습니다. 상관없으니 무시하세요. 하지만 기본적으로 여기서 하는 일은 Ghosty가 터미널 에뮬레이터 내에 내장된 스프라이트 폰트를 가지고 있다는 것입니다. 터미널이 렌더링하는 많은 글리프들이 완벽한 그리드 크기에 의존합니다. 여러분이 가장 익숙한 것은 아마도 PowerShell 유형의 글리프들, 예를 들어 화살표일 것입니다. 폰트를 사용하면, 지금 터미널에서 이것을 보고 있다면, 화살표가 그리드 크기와 완벽하게 일치하지 않는 폰트를 사용하고 있는 겁니다. 그러면 터미널이 이 작업을 하고 있지 않은 겁니다. Ghosty가 하는 일은 이러한 글리프 코드 포인트를 현재 그흔하지 않습니다. 일부 터미널에서 이렇게 하지만 문제는 어떻게 테스트하냐는 것입니다. 어떻게 작동하는지 확인할 수 있을까요? 특히 우리가 처음 발견한 것 중 하나는 그리드 크기가 한 차원에서 홀수일 때 1픽셀 오차가 있었다는 것입니다. 그래서 방법 중 하나는... 또한 새로운 글리프가 계속 추가됩니다. 우리는 래스터화 방식의 라이브러리를 변경했고 그런 것들을 바꿨습니다. 이런 것들이 퇴보하지 않도록 어떻게 확인할 수 있을까요? 이는 스냅샷 테스팅의 완벽한 사례입니다. 우리가 하는 일은 우리가 프로그래밍 방식으로 래스터화하는 모든 글리프의 비트맵을 렌더링하고, 그것을 저장소에 직접 커밋한 다음 비교합니다. 매우 간단합니다. 그것이 스냅샷이고 우리가 비교하는 방법입니다. 코드가 보여주려 했던 것은 아래쪽에 기본 단계가 있다는 것입니다. 우리는 이 기준 데이터를 읽고, 실제 래스터화를 실행한 다음 단순히 비교합니다. 거기에는 특별한 것이 없습니다. 하지만 제가 언급했던 것 중 하나는 기준 데이터 테스팅의 유용한 특성 중 하나가 더 나은 차이점 비교라는 것입니다. 스냅샷 테스팅은 더 나은 차이점을 보여줍니다. 이 경우, 우리의 비교 대상이 이미지이기 때문에 표준 이미지 차이 비교 기술을 적용할 수 있습니다. 우리는 실제로 저장소에서 이렇게 합니다. 테스트가 실패하면 차이점도 생성하여 테스트 실행의 일부로 파일 시스템에 덤프합니다. 다시 말하지만, 이것이 조오른쪽에 실패가 발생했습니다. 오른쪽의 차이점은 보기 좀 어렵지만, 거기에 초록색 줄이 있습니다. 이것이 생성된 차이점입니다. 여기서 강조하고 싶은 점은 이것이 더 유용한 차이점이라는 것입니다. 이전에는 일부 테스트가 있었고 단순히 일치하지 않는다거나 이 위치의 이 픽셀이 잘못되었다고 말했습니다. 스냅샷 테스팅이 제공하는 한 가지는 스냅샷은 보통 단일 assertion보다 더 많은 맥락을 가진다는 것입니다. 예를 들어, 이 한 픽셀이 잘못되었지만, 여기 모든 픽셀이 있어서 볼 수 있습니다. 보통 유닛 테스트 환경의 표준 assertion은 더 큰 맥락을 포함하지 않습니다. 스냅샷은 그렇게 하는 경향이 있어서 이 경우, 수직 막대가 문제라는 것을 매우 명확하게 볼 수 있었습니다. 여러 글리프에 걸쳐 있다는 것을 볼 수 있었고, 코드베이스에 대한 제 경험으로, 이런 것을 볼 때 즉시 수직 막대를 그리는 공통 함수가 잘못되었다고 생각합니다. 그리고 그것이 제가 결국 여기서 망가뜨린 것입니다. 이는 이미지에만 적용되는 것이 아닙니다. 이미지는 쉬운 시각적 예시를 제공합니다. 하지만 예를 들어, Terraform의 경우, 약 10년 전에 우리가 마주친 한 가지는 Terraform이 수행하는 첫 번째 단계가 실행될 리소스 그래프를 구축한다는 것이었습니다. 우리는 그 그래프를 구축하는 것에 대한 많은 테스트를 가지고 있었고 이 예상된 노드, 이 예상된 엣지가 존재하지 않는다는 실패를 받곤 했고 매우이런 주장들로 인해 우리에게 차이점 찾기가 매우 어려웠습니다. 그래서 결국 예상 그래프와 실제 얻은 그래프를 덤프하기 시작했고 누락된 엣지는 빨간색, 추가 엣지는 녹색, 정점 등으로 색상을 표시한 dot 형식을 생성했습니다. 그러면 전체 그래프를 로드할 수 있어 이런 것들을 디버깅하는 것이 훨씬 더 쉬워졌습니다. 이는 실제로 텍스트 비교 형식이었습니다. 이미지로 렌더링할 수 있지만 단순히 텍스트 비교였습니다. 하지만 다시 말하지만, 정점 하나가 누락되었지만 모든 정점을 볼 수 있어 주변의 더 큰 맥락을 파악할 수 있었기 때문에 디버깅이 더 쉬워졌습니다. 이것이 실제로 스냅샷 테스팅의 더 큰 이점이라고 생각합니다만, 명심해야 할 점입니다. 좋습니다, 다음 섹션은 여기에 넣으려고 하지 않았으므로 실제로 건너뛰겠습니다. 그것도 GopherCon 발표와 겹치므로 그쪽을 참고하시면 됩니다. 또한 일종의 주관적인 내용이라 사람들이 화를 내는 경향이 있습니다. 그래서 괜찮습니다. 어쨌든 건너뛰는 게 좋겠습니다. 좋습니다, 테스트 가능성에 대해 이야기해 봅시다. 비누와 거품: 부작용 격리하기. 사실 이 발표에서 다른 부분에 주의를 기울이지 않더라도, 이것이 코드를 테스트 가능하게 만드는 가장 중요한 전략입니다. 이는 다양한 맥락에서 반복해서 나타납니다. 시나리오는 이렇습니다: 테스트하고 싶은 동작이 있지만 어떤 종류의외부 입출력이나 복잡한 시스템으로 가득 찬 부작용 유형의 동작이 있습니다. 이는 매우 흔한 경우로 테스트할 수 없습니다. 여기서 목표는 이 입출력 복잡성과 상태 수프 안에서 순수한 기능적 동작을 찾아내어 추출하고 재정렬하여 대부분 테스트할 수 있는 것을 얻는 것입니다. 이 경우 대부분의 복잡성을 테스트할 수 있습니다. 결과적으로 명백히 테스트 가능한 것을 얻게 됩니다. 예를 들어보겠습니다. 명백히 테스트 가능한 것을 얻지만, 이는 쓰레기 입력, 쓰레기 출력 유형의 테스트가 됩니다. 이는 이 경우 외부 부작용을 시뮬레이션할 수 없기 때문입니다. 그래서 인위적으로 입력을 제공해야 합니다. 쓰레기를 입력하고 테스트하면 쓰레기가 나옵니다. 이런 종류의 테스트에 제공하는 입력이 실제로 현실적인지 확인해야 합니다. 이는 개념적인 것입니다. 실제로 살펴보겠습니다. 자, 여기 예시가 있습니다. 시각적으로 시작하고 코드를 볼 것입니다. 하지만 슬라이드 크기로 인해 코드를 보기는 어려울 것 같습니다. 시각적으로 이해해 봅시다. 여기 Go에서 가져온 단순화된 실제 예시가 있습니다. 터미널 에뮬레이터가 해야 할 주요 작업 중 하나는 아마도 가장 중요한 것인데, 터미널에 무언가가 입력되면키보드에서 무언가를 누르면 그것을 어떤 형식으로 인코딩해야 합니다 그리고 그것이 셸이나 실행 중인 프로그램으로 전송됩니다. 그 다음에 기본적인 작업을 수행합니다. 예를 들어 셸 프롬프트에서 Control R을 누르면 보통 역방향 검색이 나타날 것으로 예상합니다. 등등. Ghosty의 초기 버전에는 위 이미지와 같은 키보드 입력 핸들러가 있었습니다. 기본적으로 여기서 일어나는 일은 사용자가 키를 누르면 마우스 상태를 읽습니다. 이상해 보이지만 마우스 상태에 따라 키를 인코딩할지 여부가 결정됩니다. 예를 들어 실제로 무언가를 하이라이트하고 있는 중에 특정 키를 누르면 하이라이트를 이동하거나 변경하고 싶을 수 있습니다. 그래서 먼저 마우스 상태를 확인하고 응답합니다. 그 다음 키보드 상태를 확인합니다. 어떤 키가 눌렸는지, 어떤 수정자가 눌렸는지 알아야 하기 때문입니다. 반복인지, 첫 번째 누름인지 등 그런 것들을 처리합니다. 그 다음 터미널 설정을 읽습니다. 인코딩 방식에 영향을 미치는 다양한 설정이 있습니다. 레거시 인코딩을 사용하는지, 각각에 대해 kitty 인코딩을 사용하는지? 대체 유니코드 코드 포인트를 인코딩하는지? 컨트롤을 다르게 인코딩하는지? 많은 것들이 있습니다. 마지막으로 키를 인코딩하고 실제 PTY에 작성합니다. 이것은 테스트되지 않았습니다 마우스 키보드 상태 등을 설정하는 것이 간단하지 않기 때문입니다. 처음에는 이것을 테스트할 수 없는언젠가는 VM을 실행하거나 입력을 합성하고 무언가를 주장할 것이라고 생각했습니다. 그냥 미뤄두고 나중에 해결하기로 했죠. 하지만 이 코드로 인해 계속 곤란을 겪으면서 지속적인 회귀가 발생했습니다. 복잡성이 너무 높아서 지금 당장 VM 기반 테스트라도 해야 한다는 걸 깨달았습니다. 그래서 앉아서 이걸 테스트 가능하게 만들어야 한다고 집중했죠. 이대로는 안 된다고 생각했거든요. 그리고 깨달은 것이 이 색깔들이 보여주는 것입니다. 색깔을 볼 수 있다면, 노란색은 외부 로직에 의존하는 부분이고 초록색은 그렇지 않은 순수한 부분입니다. 입력을 받아 출력을 내고 외부 시스템을 건드리지 않는 부분이죠. 색상으로 구분되어 있어 쉽게 볼 수 있습니다. 함수 호출을 별도 카테고리로 단순화하고 번갈아 배치했습니다. 하지만 실제로는 이 모든 것이 뒤섞여 있었고 필요할 때마다 상태를 가져와 조건문을 실행했습니다. 적어도 제게는 이 코드를 몇 시간 동안 뚫어지게 쳐다봐야 무언가가 보이기 시작했습니다. 그리고 아래와 같은 모습이 나타났죠. 외부 상태를 가져오는 모든 부분을 맨 위로 옮기고 읽기-처리-쓰기 순서로 바꾸면 그 부분을 분리해서 인위적인 입력을 제공할 수 있고, 그렇게 하면 중간 부분을 격리시킬 수 있습니다.이 순수한 녹색 요소를 테스트해보세요. 입력만 받고 출력만 제공합니다. 그러면 테스팅 101처럼 예상 결과를 확인하는 1+2=3 같은 환경이 됩니다. 이 경우 대부분의 복잡성, 버그, 문제가 이 녹색 요소에 있었습니다. 그래서 우리는 많은 문제를 극적으로 제거할 수 있었습니다. 또한 퍼징 등을 훨씬 쉽게 할 수 있게 되었죠. 퍼징에 대해서는 여기서 말하지 않겠습니다. 충분히 네, 예상한 대로입니다. 이것이 키 인코더의 실제 코드입니다. 아래쪽을 보여드리고 싶네요. 아래쪽이 더 중요하지만, 위쪽만 보이는데 각 줄은 보이지만 텍스트는 안 보이죠. 각 줄은 구조체, 불리언, 정수, 문자 등 키 인코딩에 필요한 상태 정보입니다. 여기서 보여주려는 것은 터미널이 유효한 키 인코딩을 생성하는 데 실제로 필요한 상태의 양입니다. 총 약 15개의 필드가 있습니다. 일부는 운영체제에서, 일부는 터미널의 내부 설정에서 생성됩니다. 아래쪽은 실제 테스트를 그대로 복사한 것으로, 수정 없이 우리가 이제 테스트할 수 있는 회귀 유형을 보여줍니다. 아래쪽 테스트가 하는 일은 믿으셔야 하고, 나중에 슬라이드를 확인하실 수 있습니다. 특정 입력을 어떻게 인코딩하는지 테스트하고 있습니다.러시아어 키보드 레이아웃과 kitty 키보드 설정을 인코딩하여 대체 유니코드 문자도 추가하고 예측합니다 우리가 올바른 결과를 얻을 것으로 예상합니다. 이것이 제가 이걸 테스트해야 했던 이유입니다 왜냐하면 기능을 수정하거나 버그를 고치거나 기능을 구현할 때마다 저에게는 매우 낯선 레이아웃에서 문제가 발생했기 때문입니다 당연히 저는 러시아어를 타이핑하지 않았죠. 그리고 이 특정 kitty 레이아웃을 얻는 것은 매우 어렵습니다. 이는 매우 흔한 일입니다. 러시아어, 일본어, 중국어, 헝가리어 등 모든 종류의 매우 언어 특정적인 테스트 케이스가 있어서 우리가 항상 올바르게 처리하는지 확인합니다. 그리고 이 때문에 Go See는 가장 완벽하고 안정적인 키 인코딩 기능 중 하나를 가지고 있다고 생각합니다 이는 많은 도움이 되었습니다. 이것이 바로 부작용을 분리하는 핵심 포인트입니다. 기본적으로 여기서부터는 계속해서 이런 예시를 보여줄 것입니다 스냅샷 테스팅을 하고, 부작용을 분리하고 이들을 결합하여 점점 더 복잡한 작업을 수행할 것입니다 . 좋습니다. GPU에 대해 이야기해 보겠습니다. GPU에 대해 정말 흥미로운 시기입니다. 다행히도 저는 OpenAI에서 일하지 않기 때문에 이 발표에서 AI에 대해 전혀 언급하지 않을 것입니다 일반적인 GPU 프로그래밍에 대해서만 이야기하겠습니다. 이는 적용될 수 있습니다AI에도 적용되지만, 제 경우엔 렌더링과 일부 연산에 적용됩니다. GPU 프로그래밍과 GPU 테스팅에 대해 이야기하고 싶습니다. 배경 설명을 하자면 Ghosty는 GPU로 렌더링되는 터미널 에뮬레이터입니다. 터미널을 실행할 때 보이는 주요 화면, 커서가 깜빡이는 그 전체가 단순히 GPU를 통해 렌더링하는 이미지입니다. 우리는 GPU에서 약간의 연산도 수행합니다. 개인적으로는 몇 년 전 처음으로 GPU 코드를 작성해보는 시도였습니다. 그 전까지는 순수하게 CPU에서만 작업했죠. 그래서 시작할 때 제가 가장 먼저 물었던 것은 "이걸 어떻게 테스트하지?"였습니다. 처음 든 생각과 반응은 전형적인 "테스트 불가능"이었죠. VM을 띄우고 스크린샷을 찍는 게 매우 명확한 해결책처럼 보였습니다. 결국 렌더링을 하는 거니까요. 그래서 그게 올바른 해결책처럼 보였습니다. 하지만 수년간 우리 렌더러에는 단위 테스트가 없었습니다. 그리고 수년간 키 인코딩과 마찬가지로 렌더러와 GPU 코드에서 계속해서 회귀 오류를 잡느라 고생했습니다. 그래서 마침내 저는 앉아서 이걸 해결해야겠다고 마음먹었습니다. GPU 로직을 테스트하는 방법에 대한 자료는 놀랍게도 부족합니다. GPU 테스트 방법을 웹 검색해보면, 구글 첫 페이지가 완전히 쓸모없는 경우가 드물게 있는데 이게 그 중 하나입니다. 한 응답에 약간 괜찮은 아이디어가 있긴 한데흥미롭지만, 아무도 구현하지 않은 아이디어에 불과했습니다. 그래서 어떤 방향으로 가야 할지 스스로 알아내야 했죠. 그래서 저는 Kronos Group, Apple, Microsoft로 가서 DirectX, Direct3D, Metal, OpenGL, Vulkan의 레퍼런스 자료와 문서들을 큰 텍스트 형식으로 제공되는 모든 것을 다운로드했습니다. 그리고 Command F 검색과 LLVM 지원 검색을 했습니다. 테스트, 검증, 스냅샷, 버그, 안정성 등 이런 용어들을 모두 찾아봤죠. 놀라운 점은 세 회사의 언어 명세서, 드라이버 사양 등을 포함한 전체 참고 자료가 PDF로 약 4,000페이지에 달하는데도 그 용어들이 한 번도 언급되지 않았다는 겁니다. 단 한 번도 나오지 않았어요. 제가 찾은 바로는, GPU 테스팅 방법에 대한 공식 자료가 전혀 없었고, 이 문제에 관심을 가진 사람도 거의 없었습니다. 적어도 공개적으로 인덱싱된 자료는 없었죠. 그래서 저는 이걸 도전으로 받아들였습니다. 저는 테스팅을 좋아하니까 어떻게든 해결할 수 있을 거라고 생각했죠. 그리고 제게 꽤 잘 맞는 방법을 찾아냈다고 봅니다. 여기서 간단한 면책 조항을 추가하자면, 제 경험에서 알 수 있듯이 저는 아마도 이 분야의 전문가는 아닙니다.GPU에 대해 전문가는 아닙니다. 복잡한 3D 게임을 만들어본 적이 없어요. 제 터미널 에뮬레이터가 어드벤트 오브 코드 예제 같은 장난감 수준을 넘어서 GPU를 사용한 거의 유일한 것입니다. 제 기술이 실제로 잘 일반화되는지는 확실하지 않습니다. 제 변명을 하자면, Ghostly에는 약 15,000줄의 렌더러 코드가 있습니다 Metal과 OpenGL로 나뉘어 있죠. 두 시스템에 대해 별도의 렌더러가 있고, 여기에는 CPU 코드와 GPU용 셰이더가 모두 포함됩니다 셰이더는 약 2,000줄입니다. 그래서 CPU에 13,000줄, GPU에 2,000줄이 있습니다. 이것이 제가 다루던 내용입니다. 이것이 제가 테스트하려던 것입니다. 제가 깨달은 핵심은 GPU 프로그래밍에는 두 가지 측면이 필요하다는 것입니다. 배경 설명을 하겠습니다. 데이터를 준비하고 결과를 처리하는 CPU 측면과 실제로 셰이더를 실행하는 GPU 부분이 있습니다. 우리는 이 둘을 분리해서 테스트할 수 있다고 느꼈고, 특히 GPU는 순수한 함수 평가기라는 점이 명확해서 테스트가 매우 쉽지만, 워크로드를 설정하는 것은 매우 어렵다는 점이 흥미로운 점입니다. 하지만 이제 이것을 시각적으로 더 자세히 살펴보고 GPU 측면부터 시작해보겠습니다. CPU 측면. 우리는 이 기본적인 내용을 실제로 읽을 수 있습니다. GPU 프로그래밍에 익숙하지 않은 분들을 위해 배경 설명을 하자면, CPU도 GPU를 준비하는 데 일부 작업을 수행해야 합니다. CPU는 올바른 데이터와 기본적으로 이런 작은 작업 설명들을 결국에는 GPU에 오프로드하고 제출하여 "여기 처리할 것들이 있으니 실행해"라고 말합니다. "나중에 돌아오거나, 완료되면 알려주세요. 나중에 결과를 확인하겠습니다." 이것이 GPU를 생각하는 매우 일반적인 방식입니다. GPU를 위해 CPU가 수행하는 준비 작업은 대략 이 최상위 함수로 생각할 수 있습니다. 이 함수는 세계의 상태를 입력으로 받습니다. 렌더링을 하는 경우, 이를 장면 상태라고 부를 수 있습니다. 레벨에 어떤 몬스터가 있는지, 그들의 위치, 카메라의 위치, 플레이어의 위치 등입니다. 내가 어느 행성에 있는지. 이것이 존재하는 장면 상태입니다. 장면 상태를 가져와서 결과적으로 세 가지 값 집합을 생성합니다. 그래프(정점과 간선)를 생성하고, 그 그래프에 대한 데이터 첨부를 생성합니다. 어떤 노드가 어떤 데이터에 접근해야 하는지 말이죠. 이것이 GPU입니다. 그래프는 단순히 그래프입니다. 정점과 간선이 있고, 정점은 연산입니다. 정점 셰이딩, 프래그먼트 셰이딩, 컴퓨트 셰이딩 등과 같은 것들이죠. 간선은 단계 간의 데이터 의존성입니다. 정점 셰이더는 프래그먼트 셰이더가 필요로 하는 출력을 생성하거나, 정점 셰이더가 필요로 하는 것, 또는 프래그먼트 셰이더가 특정 텍스처에 접근해야 하는 것 등입니다. 이런 간선들이 그래프에 존재하고, 데이터 첨부는 말 그대로 바이트 버퍼입니다. 역사Vulcan, Metal, 그리고 나중의 Direct3D와 같은 더 현대적인 그래픽 API들은 이제 이것들을 단순히 버퍼라고 부르는 경향이 있습니다. 실제로 이는 바이트의 집합일 뿐이기 때문입니다. 때로는 이 바이트들이 구조를 가집니다. 차원과 보폭을 가진 RGBA 형식으로 텍스처가 됩니다. 하지만 때로는 단순 계산을 위한 바이트일 뿐입니다. 따라서 데이터는 단순히 바이트이며, 원하는 대로 활용할 수 있습니다. CPU 측을 테스트하기 위해서는 부작용을 분리하는 기술을 적용해야 합니다. 이 경우 부작용은 워크로드를 준비하고 제출하기 위해 GPU 자체에 대한 모든 API 호출입니다. 그래서 제가 결국 한 일은 중간 단계를 만드는 것이었습니다. 장면 상태를 가져와 그래프와 데이터를 생성하고, 그 다음 그래프가 올바른 형태를 가지고 있는지 확인합니다. 저는 테라폼에 대한 많은 경험이 있습니다. 데이터에 대해서는 스냅샷 테스팅을 다시 가져왔습니다. 단순히 바이트이기 때문입니다. 따라서 우리는 구조화된 스냅샷 검증을 할 수 있습니다. 그 후에는 간단한 테스트되지 않은 코드가 있는데, 이는 그래프와 데이터 첨부를 GPU API 호출로 변환합니다. 이 부분은 거의 변경되지 않습니다. 엔드투엔드 테스트가 있을 때까지 이를 테스트하지 않아도 괜찮습니다. 이렇게 해서 우리는 특정 장면 상태가 주어졌을 때 GPU에 대해 올바른 워크로드를 생성하고 있는지 테스트할 수 있습니다. GPU가 이를 가지고 올바른 작업을 수행할지는이것은 go see가 장면을 렌더링하기 위해 사용하는 15,000줄의 코드 중 13줄에 불과합니다. 이는 매우 큰 작업입니다. GPU 측면에서는 시각적으로 훨씬 단순합니다. GPU는 디스크에 접근할 수 없고, 네트워킹에 접근할 수 없으며, 다른 주변 장치에 접근할 수 없습니다. 오직 자체 메모리에만 접근할 수 있죠. 따라서 GPU는 정의상 거의 순수한 함수 평가기입니다. 연산과 데이터가 있고 데이터를 출력합니다. 이는 테스트하기에 매우 좋고 쉬운 것입니다. 하지만 재미있는 점은 제가 말씀드렸듯이, 어려운 부분은 실제로 작업량을 제출하는 것입니다. 그래서 GPU 측면의 일반적인 아이디어는 파이프라인이 기대하는 입력 버퍼 세트를 인위적으로 구성하는 것입니다. 출력 버퍼가 CPU에서 읽을 수 있도록 할 것입니다. CPU로 돌아오지 않을 수 있는 프레임 버퍼에 쓰는 대신, 이 화면을 렌더링할 것입니다. 화면에 렌더링하지 말고, 제가 가진 CPU에서 읽을 수 있는 다른 메모리에 렌더링하세요. 그런 다음 실제 GPU 작업을 제출합니다. 우리는 CPU에서 유닛 테스트를 실행합니다. CPU에서는 GPU를 위한 유닛 테스트를 실행하고, GPU에서는 실제 GPU 작업을 제출한 다음 출력 버퍼를 비교합니다. 대략적으로 말하자면, 스냅샷 테스팅, 실제 데이터 파싱, 원하는 방식으로 하지만 어떤 식으로든 출력을 비교합니다. 이것이 일반적인 아이디어입니다. 어렵죠, 그렇죠? 그래서 실제로 제가 발견한 것은29:56.600 출시한 것은 스냅샷 테스팅을 통한 전체 렌더 패스입니다. 제가 하는 일은 인위적으로 장면 상태를 만드는 것인데, 보통 아주 작은 터미널, 2x2 터미널 같은 것입니다. 이를 실제 GPU로 보내고 이미지를 받아 비교합니다. 픽셀 단위로 동일한 이미지를 기대합니다. 이는 일종의 엔드투엔드 테스팅처럼 들립니다. 어떤 면에서는 그렇습니다. 정확히 단위 테스트는 아니고 통합 테스트에 가깝지만, 매우 강력하고 견고한 테스트라고 생각합니다. 두 가지 이유가 있습니다. 첫째, 훨씬 빠릅니다. 창을 만들고, GPU 작업을 제출하고, 스크린샷을 찍어 비교하는 것이 현대 컴퓨터에서는 순식간에 이루어집니다. 정말 빠르고 매우 견고합니다. 이 경우 입력 장면 상태를 엄격히 제어합니다. 한 번의 렌더 패스를 실행하고 정확히 한 프레임의 결과를 얻어 비교합니다. 일반적인 엔드투엔드 테스트에서 걱정해야 할 타이밍, 합성 입력, 윈도우 위치, 크로밍 등 주변의 모든 것들을 신경 쓸 필요가 없습니다. 비교하려는 내용에 대해 완벽하게 잘린 이미지를 정확히 얻고 단일 프레임이므로 매우 견고합니다. 잘 작동합니다. 향후에는 셰이더를 더 고립된 상태에서 테스트하고 싶습니다. 이미 많은 작업을 했고, 개념 증명으로 수행한 몇 가지를 설명해 드리겠습니다. 잘 작동합니다.하지만 모두 만족스럽지 않은 트레이드오프가 있습니다. 그래서 실제로 이것을 배포하지는 않았습니다. 이것이 제 면책 조항입니다. 개념 증명은 모두 작동합니다. 전체 렌더 패스는 당연히 전체 입력을 이미지로 테스트하지만, 개별 셰이더 자체는 꽤 복잡할 수 있거나 매우 복잡할 수 있습니다. 조건문, 루프, 명백한 엣지 케이스들이 있어서 어떤 방식으로든 테스트하고 싶습니다. 그리고 때로는 초기 장면 상태를 통해 이러한 엣지 케이스를 유도하거나, 적어도 결과 출력 상태에서 시각화하기 어렵습니다. 그래서 저는 셰이더로 단위 테스트에 더 가까워지고 싶습니다. 그래서 저는 최선의 방법을 찾기 위해 다양한 기술을 시도해 왔습니다. 다시 말하지만, 이것들은 많은 사람들이 시도하지 않은 것들입니다. 각각에 대해 자세히 설명하지는 않겠습니다. 아직 배우고 있기 때문입니다. 하지만 높은 수준의 개념만 다루겠습니다. 이에 대해 더 자세히 아시는 분들이 있다면 듣고 싶습니다. 예를 들어, 몇 가지 개념만 말씀드리겠습니다. OpenGL. 많은 사람들이 Vulkan 등에 대해 열광하지만, OpenGL도 여전히 작동합니다. OpenGL에는 트랜스폼 피드백이라는 기능이 있는데, 이것은 기본적으로 일부 셰이더의 출력을 CPU가 읽을 수 있는 버퍼로 캡처할 수 있게 해줍니다. 모든 셰이더는 아니지만 일부 유형의 셰이더에 대해서요. 실제로 매우 좋은 API입니다. 문자열을 추가하고 원하는32:48.220출력 버퍼로 만들어집니다. 그리고 그게 완벽해요. 저는 이게 무엇을 위해 만들어졌는지 모르겠어요. 본 적이 없거든요. 제가 소스 그래프 검색 등을 해봤는데, 거의 사용되지 않는 것 같아요. 매우 적은 API 호출만 있고, Vulkan에는 실제로 존재하지 않아요. 명확히 충분한 가치가 없어서 발전시키지 않은 것 같아요. 하지만 일종의 셰이더 테스트에는 완벽해요. 그게 문제죠. 이건 Metal 쪽에서만 적용돼요. Metal은 변환 셰이더가 없고, Direct3D도 마찬가지예요. 그래서 제가 발견한 해결책은 공유 로직을 컴퓨트 셰이더로 추출하는 거예요. 렌더링이 아닌 순수 컴퓨트 셰이더로, 각 측면에서 이 공유 라이브러리를 호출하고 컴퓨트 셰이더로 실행해 제 나름의 변환 피드백 메커니즘을 만드는 거죠. 이는 더 많은 코드가 필요해요. GPU 코드의 구조 변경이 필요한데, 테스트에는 표준이지만 그리 좋지는 않아요. 그래도 작동해요. 좋은 점은 모든 종류의 셰이더에 적용된다는 거예요. 하지만 컴퓨트 셰이더로 추출할 수 있어야 하는데, 불가능한 경우를 본 적이 없어요. 많은 가능성이 있죠. 안타깝게도 미래 측면의 제품 상태까지는 도달하지 못했어요. 언젠가 블로그나 강연에서 모든 것을 정리해서 이야기할 수 있길 바라요. 하지만 이 결과로 우리가 렌더러를 테스트할 수 있다고 확신해요. 제가 가진 전체 렌더 패스 방식은 매우 견고하고 빠릅니다. 실제로 GPU 같은 드라이버가 작동한다고 가정하고 드라이버를 검증하려는 게 아니라면, 소프트웨어 드라이버로만 실행하면 됩니다. 그리고 한 프레임만 실행하고 있습니다. 그래서 매우 빠릅니다. 그리고 흥미롭고 스냅샷과 부작용 격리를 잘 보여준다고 생각합니다. 좋습니다, 마지막으로 이야기하고 싶은 주제는 VM 테스팅입니다. 특히 이전에 언급한 노란색 박스를 테스트하려면 이것이 필요합니다. 실제로 테스트하는 유일한 방법은 실제로 그것을 발생시키는 것입니다. 키보드와 마우스 및 다른 유형의 이벤트를 시뮬레이션하는 유일한 방법은 VM을 통해서입니다. 키보드나 마우스뿐만 아니라 네트워크 장애 같은 것도 안티테시스가 정말 잘합니다. 디스크 장애, 이런 것들도요. 하이퍼바이저 계층을 통해 가장 잘 수행됩니다. 여기서 사과드릴 게 있는데 닉스를 언급할 거라서요. 닉스에 대한 열정이 많은 사람들을 지치게 한다는 걸 알고 있고 그것에 대해 듣고 싶어 하지 않는다는 것도 압니다. 그래서 죄송합니다. 제 변명을 하자면, 저는 개발 테스트 환경, 가상화, 컨테이너화에 대해 잘 알고 있다고 자신합니다. 15년 동안 제 경력의 전부였거든요. 그리고 제가 곧 보여드릴 것을 잘 구현할 수 있는 다른 기술은 모르겠습니다. 그래서 닉스를 사용할 것이고할 겁니다. 미안하지만 동시에 미안하지 않습니다. 좋습니다. 먼저 Nix 없이 VM 테스팅에 대해 이야기해 봅시다. 감정적인 반응이 있다면, 여기서부터 시작하겠습니다. 좋습니다. 말씀드렸듯이 엔드 투 엔드 테스트가 필요한 것들이 있습니다. 그리고 VM이 그것에 가장 적합합니다. 컨테이너로도 가능할 수 있지만, 원하신다면 서로 바꿔 사용하셔도 됩니다. 하지만 이 경우에는 계속 VM이라는 단어를 사용하겠습니다. VM 테스팅은 정말 복잡하고 거의 임의로 복잡한 세계의 상태를 모델링할 수 있게 해줍니다. 특정 커널 소프트웨어 버전, 특히 그들 간의 상호작용을 포함해서요. 데스크톱 측면에서는 다양한 로케일, 다양한 키보드 레이아웃 등을 시뮬레이션할 수 있게 해줍니다. 이런 것들이 때때로 필요합니다. 그래서 전체 VM을 실행하고, 실제로 소프트웨어를 실행하고, 이벤트를 합성한 다음 원하는 일이 일어났는지 어떻게든 확인하는 것이 아이디어입니다. 보통 스크린샷이나 SSH 명령을 통해 이루어집니다. SSH 명령은 이 프로세스가 실행 중이다, 이 파일이 존재한다, 이 파일에 이런 내용이 있다 등을 확인합니다. 주로 이 두 가지 방식으로 수행됩니다. 좋습니다. 이제 Nix를 도입해 봅시다. 왜 Nix를 도입해야 할까요? 실제로 이것을 읽을 수 있어 다행입니다. Nix를 도입하는 이유는 Nix가 Nix에 접근할 수 있는 완전한 자체 테스팅 프레임워크를 제실제로 Nix 자체를 테스트하기 위해 일차적으로 사용되기 때문입니다. 그래서 없어지지 않을 겁니다. 매일 실행되고 있죠. 지금도 실행 중입니다. 이런 종류의 테스트를 실행하기 위해 Nix 프로젝트에 의해 지금 수천 개의 작업이 대기 중입니다. 둘째, 이것은 실제 테스트 프레임워크입니다. VM을 시작하는 방법만 정의하는 게 아닙니다. 테스트를 작성하고 통과를 확인하는 전체 API가 있습니다. 이것이 중요한 이유는 단순히 Docker 이미지를 실행하는 것과 같지 않기 때문입니다. Docker는 컨테이너의 런타임을 제공하지만, 실제로 테스트할 수 있는 도구는 제공하지 않습니다. 이것은 양쪽 모두를 제공합니다. 그리고 셋째, 죄송합니다, 하나 돌아가겠습니다. Nix로 구동되기 때문에 이는 이점입니다. 완전한 접근이 가능하니까요. 솔직히 말해서 언어는 아마 단점일 겁니다. 하지만 Nix 패키지에 대한 접근은 이점입니다. 기본적으로 지난 수십 년간 존재했던 모든 준인기 소프트웨어의 모든 버전에 접근할 수 있기 때문입니다. 이는 매우 중요합니다. 커널, libc 등 모든 것의 특정 버전을 고정할 수 있기 때문입니다. 이것이 제가 찾은 유일한 방법입니다. 가장 성가신 데스크톱 사용자인 Debian 사용자들이 오래된 장기 지원 소프트웨어 버전을 가져와서 이게 작동하지 않는다고 말할 때, 이것이 실제로 작동한다는 것을 확인할 수 있는 유일한 방법이었습니다. 그럼 이것이 어떻게 생겼는지 살펴보겠습저는 선택적 복수형을 사용했습니다. Nix를 사용하면 여러 머신을 정의하고 그들 간의 네트워킹을 할 수 있기 때문입니다. 하지만 그건 다루지 않을 겁니다. 그것만으로도 전체 강연 주제가 될 수 있죠. 아마 머신 구성에 대한 전체 학위 과정이 될 수도 있을 겁니다. 이건 그저... 여기서 인용 부호가 많은 의미를 담고 있습니다. 이것은 단지 Nix OS 구성일 뿐입니다. 전체 NIX 설치를 구성할 때 넣을 수 있는 모든 것을 여기에 넣을 수 있습니다. 즉, 말씀드렸듯이 모든 것을 의미합니다. 커널, 드라이버, 사용자, 패키지, 모든 것이죠. 두 번째 단계는 실제로 테스트를 정의하는 것입니다. 테스트는 Python으로 작성되며, nix로 작성되지 않습니다. 여기에 문자열을 넣거나 테스트가 포함된 파일을 포함시킵니다. Nix는 완전한 Python API를 제공하여 systemd 유닛 대기와 같은 좋은 고수준 기능을 제공합니다. Nix OS가 systemd를 사용하기 때문이죠. 심지어 OCR도 할 수 있습니다. 화면에 특정 텍스트가 나타날 때까지 기다릴 수 있고 이를 자동으로 처리합니다. 그리고 이건 그저 Python일 뿐입니다. 이를 통해 얻을 수 있는 것 중 하나는 REPL에 접근할 수 있다는 것입니다. VM을 실행하고 REPL을 사용하여 테스트를 실험해볼 수 있습니다. 그래서 좋죠. 이 경우 우리는 ALICE 사용자에 대한 테스트를 정의하고 있습니다. 앞서 언급하지 않았지만, 이전에 Alice를 위해 Firefox를 설치했습니다. ALICE 사용자는 Firefox를 가지고 있고 Nix에 대해 이야기할 때마다 해야 하듯이 말이죠. 하지만 기본적으로 중요한 점은 프레임워크 런타임에 내장된 메커니즘이 있다는 것입니다 테스트를 실행하고, REPL을 얻고, 테스트를 디버깅하고, 개발하고, 단일 테스트를 실행하는 등의 모든 것이 일종의 단일 명령으로 가능합니다. 그래서 이것은 VM 테스트를 처리하기 위한 완전한 엔드투엔드 스레드입니다. 그리고 제가 말씀드렸듯이, 이 정도 수준의 유연성을 제공하는 다른 것을 찾지 못했습니다 테스팅에 초점을 맞춘 것 말이죠. 맞아요. 수많은 프레임워크가 있고, 제가 일부를 만들기도 했습니다 머신을 스핀업하고 VM을 스핀업하는 것들이요. 하지만 실제로 엔드투엔드 부분을 완성하는 것은 아니었죠. 제가 실제로 NixOS VM을 어디에 사용하고 있나요? 다시 말하지만, 완전한 샌드박스 환경 없이는 테스트할 수 없는 것들입니다. 제가 이것을 하게 된 실제 계기는 무엇이었을까요? 그것은 바로 입력 방식이었습니다. 저는 꽤 차분한 사람이지만, 리눅스의 입력 방식 때문에 몇 번 테이블을 내리쳤습니다. 모르시는 분들을 위해 설명하자면, 입력 방식은 기본적으로 특정 아시아 언어를 입력하는 방법입니다. 이모지 키보드도 일종의 입력 방식이죠. 물리적 키보드에 없는 문자를 입력하는 능력이 바로 입력 방식입니다. 필기 입력도 입력 방식의 일종이죠. 리눅스에서는 입력 방식, 입력 방식 프레임워크, 윈도우 시스템/컴포지터가 모두 다른 사람들에 의해 개발됩니다.41:26.이는 리눅스의 어려움이 정말 두드러지는 부분이라고 생각합니다. 매우 특정한 버전의 다양한 것들이 완전히 다르게 작동하기 때문입니다. 이는 저를 미치게 만들었죠. 그래서 입력 방식을 테스트하기 위해 VM 테스팅이 필요했습니다. 저는 리눅스를 좋아하고 NEXT OS를 사용하지만, 애플과는 대조적입니다. 애플에서는 모든 것들이 함께 동기화되어 작동해야 한다는 명확한 수직적 지시가 있습니다. 앱 개발자로서는 훨씬 쉽지만, 아시다시피, 제가 다뤄야 할 것들이죠. 그리고 데스크톱 통합 같은 다른 것들도 있습니다. Open과 Ghostly가 우클릭 시 나타나는지 확인하는 것 등이요. 실제로 스크린샷을 찍고 우클릭하고 다시 스크린샷을 찍지 않고 어떻게 테스트할 수 있겠습니까? 이것들도 리눅스에서 다양한 버전 업그레이드로 계속 문제가 생깁니다. 그래서 VM에 완벽합니다. 여기서 리눅스에 대해 많이 불평했지만, 제 의견으로는 데스크톱 리눅스에서 안정적인 데스크톱 앱 경험을 만들기 위해 해야 하는 작업들입니다. 나중을 위해 여기 두겠습니다. 사진을 찍거나 나중에 슬라이드를 다운로드하세요. VM 테스팅에 대해 더 많이 배울 수 있는 자료들입니다. 매우 강력하지만, Nix의 모든 것처럼 학습 곡선이 수직 절벽 같습니다. 그래서 '왕좌의 게임'에 나오는 벽을 오르고 싶다면, 이 자료들이 필요할 겁니다. 제 생각에 이를 수행함으로써 얻는 이점은 큽니다. 적절한 필요한 테스트를 위해서는 이에 비할 만한 것이 없습니다. 그래서 이게 전부입니다. 감사합니다. 우리가 5가지 주제만 다뤘다는 걸 알지만, 내용이 많았다고 생각합니다. 가장 중요한 것은, 다시 말하지만, 다른 것은 몰라도 부작용을 분리하는 것이 매우 강력한 기술이라는 점이며, 저는 여러 예시를 보여드리고 싶었습니다. 그래서 그렇게 하려고 노력했습니다. 더 많은 내용을 원하시면 2017년 Go 컨퍼런스 자료를 참조하세요. 감사합니다.